mcp-grocy 2.2.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +125 -32
  3. package/build/resources/CHANGELOG.md +47 -0
  4. package/build/resources/DOCS.md +8 -3
  5. package/build/resources/README.md +125 -32
  6. package/build/resources/api-reference.md +88 -88
  7. package/build/resources/config.md +45 -17
  8. package/build/resources/examples.md +120 -221
  9. package/build/resources/installation.md +9 -14
  10. package/build/resources/response-format.md +29 -20
  11. package/build/version.js +2 -2
  12. package/package.json +50 -24
  13. package/build/api/client.js +0 -121
  14. package/build/config/index.js +0 -205
  15. package/build/main.js +0 -44
  16. package/build/server/http-server.js +0 -218
  17. package/build/server/mcp-server.js +0 -163
  18. package/build/server/resources.js +0 -60
  19. package/build/tools/base.js +0 -141
  20. package/build/tools/household/definitions.js +0 -157
  21. package/build/tools/household/handlers.js +0 -109
  22. package/build/tools/household/index.js +0 -25
  23. package/build/tools/index.js +0 -2
  24. package/build/tools/inventory/definitions.js +0 -379
  25. package/build/tools/inventory/handlers.js +0 -430
  26. package/build/tools/inventory/index.js +0 -32
  27. package/build/tools/module-loader.js +0 -151
  28. package/build/tools/recipes/definitions.js +0 -261
  29. package/build/tools/recipes/handlers.js +0 -471
  30. package/build/tools/recipes/index.js +0 -33
  31. package/build/tools/recipes/validations.js +0 -23
  32. package/build/tools/shopping/definitions.js +0 -71
  33. package/build/tools/shopping/handlers.js +0 -43
  34. package/build/tools/shopping/index.js +0 -14
  35. package/build/tools/system/definitions.js +0 -86
  36. package/build/tools/system/handlers.js +0 -94
  37. package/build/tools/system/index.js +0 -16
  38. package/build/tools/types.js +0 -1
  39. package/build/tools/validation-helpers.js +0 -36
  40. package/build/types/index.js +0 -62
  41. package/build/utils/errors.js +0 -138
  42. package/build/utils/logger.js +0 -141
@@ -1,86 +0,0 @@
1
- import { config } from '../../config/index.js';
2
- export const systemToolDefinitions = [
3
- // ==================== CORE SYSTEM UTILITIES ====================
4
- {
5
- name: 'system_locations_get',
6
- description: '[SYSTEM/LOCATIONS] Get all storage locations from your Grocy instance. Use this to find location IDs and names when working with other tools that require locationId parameters.',
7
- inputSchema: {
8
- type: 'object',
9
- properties: {},
10
- required: []
11
- }
12
- },
13
- {
14
- name: 'system_units_get',
15
- description: '[SYSTEM/UNITS] Get all quantity units from your Grocy instance.',
16
- inputSchema: {
17
- type: 'object',
18
- properties: {},
19
- required: []
20
- }
21
- },
22
- {
23
- name: 'system_users_get',
24
- description: '[SYSTEM/USERS] Get all users from your Grocy instance.',
25
- inputSchema: {
26
- type: 'object',
27
- properties: {},
28
- required: []
29
- }
30
- },
31
- // ==================== DEVELOPER UTILITIES ====================
32
- {
33
- name: 'system_dev_call_api',
34
- description: '[SYSTEM/DEV] Call a specific Grocy API endpoint with custom parameters.',
35
- inputSchema: {
36
- type: 'object',
37
- properties: {
38
- endpoint: {
39
- type: 'string',
40
- description: 'Grocy API endpoint to call (e.g., "objects/products"). Do not include /api/ prefix.'
41
- },
42
- method: {
43
- type: 'string',
44
- enum: ['GET', 'POST', 'PUT', 'DELETE'],
45
- description: 'HTTP method to use',
46
- default: 'GET'
47
- },
48
- body: {
49
- type: 'object',
50
- description: 'Optional request body for POST/PUT requests'
51
- }
52
- },
53
- required: ['endpoint']
54
- }
55
- },
56
- {
57
- name: 'system_dev_test_request',
58
- description: `[SYSTEM/DEV] Test a REST API endpoint and get detailed response information. Base URL: ${config.grocy.base_url} | SSL Verification enabled | Authentication: ${config.grocy.api_key ? 'API Key using header: GROCY-API-KEY' : 'No authentication configured'}`,
59
- inputSchema: {
60
- type: 'object',
61
- properties: {
62
- method: {
63
- type: 'string',
64
- enum: ['GET', 'POST', 'PUT', 'DELETE'],
65
- description: 'HTTP method to use'
66
- },
67
- endpoint: {
68
- type: 'string',
69
- description: 'Endpoint path (e.g. "/users"). Do not include full URLs - only the path.'
70
- },
71
- body: {
72
- type: 'object',
73
- description: 'Optional request body for POST/PUT requests'
74
- },
75
- headers: {
76
- type: 'object',
77
- description: 'Optional request headers for one-time use.',
78
- additionalProperties: {
79
- type: 'string'
80
- }
81
- }
82
- },
83
- required: ['method', 'endpoint']
84
- }
85
- }
86
- ];
@@ -1,94 +0,0 @@
1
- import { BaseToolHandler } from '../base.js';
2
- import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
3
- import apiClient from '../../api/client.js';
4
- import { config } from '../../config/index.js';
5
- import { logger } from '../../utils/logger.js';
6
- export class SystemToolHandlers extends BaseToolHandler {
7
- // ==================== CORE SYSTEM UTILITIES ====================
8
- getLocations = async () => {
9
- return this.executeToolHandler(async () => {
10
- const data = await this.apiCall('/objects/locations');
11
- return this.createSuccess(data);
12
- });
13
- };
14
- getQuantityUnits = async () => {
15
- return this.executeToolHandler(async () => {
16
- const data = await this.apiCall('/objects/quantity_units');
17
- return this.createSuccess(data);
18
- });
19
- };
20
- getUsers = async () => {
21
- return this.executeToolHandler(async () => {
22
- const data = await this.apiCall('/users');
23
- return this.createSuccess(data);
24
- });
25
- };
26
- // ==================== DEVELOPER UTILITIES ====================
27
- callGrocyApi = async (args) => {
28
- const { endpoint, method = 'GET', body = null } = args;
29
- if (!endpoint) {
30
- throw new McpError(ErrorCode.InvalidParams, 'Missing required parameter: endpoint');
31
- }
32
- // Remove leading /api/ if present
33
- const cleanEndpoint = endpoint.replace(/^\/?(?:api\/)?/, '');
34
- try {
35
- const response = await apiClient.request(`/${cleanEndpoint}`, {
36
- method,
37
- body
38
- });
39
- return this.createSuccess(response.data);
40
- }
41
- catch (error) {
42
- logger.error(`Error calling Grocy API endpoint ${endpoint}`, 'api', { error });
43
- return this.createError(`Failed to call Grocy API endpoint ${endpoint}: ${error.message}`);
44
- }
45
- };
46
- testRequest = async (args) => {
47
- const { method, endpoint, body, headers = {} } = args;
48
- if (!method || !endpoint) {
49
- throw new McpError(ErrorCode.InvalidParams, 'method and endpoint are required');
50
- }
51
- const normalizedEndpoint = `/${endpoint.replace(/^\/+|\/+$/g, '')}`;
52
- try {
53
- const startTime = Date.now();
54
- const response = await apiClient.request(normalizedEndpoint, {
55
- method,
56
- body,
57
- headers: { ...config.getCustomHeaders(), ...headers }
58
- });
59
- const endTime = Date.now();
60
- const responseObj = {
61
- request: {
62
- url: `${config.grocy.base_url}${normalizedEndpoint}`,
63
- method,
64
- headers: { ...config.getCustomHeaders(), ...headers },
65
- body,
66
- authMethod: config.grocy.api_key ? 'apikey' : 'none'
67
- },
68
- response: {
69
- statusCode: response.status,
70
- timing: `${endTime - startTime}ms`,
71
- headers: response.headers,
72
- body: response.data
73
- },
74
- validation: {
75
- isError: response.status >= 400,
76
- messages: response.status >= 400 ?
77
- [`Request failed with status ${response.status}`] :
78
- ['Request completed successfully']
79
- }
80
- };
81
- return this.createSuccess(responseObj);
82
- }
83
- catch (error) {
84
- return this.createError(`Test request failed: ${error.message}`, {
85
- request: {
86
- url: `${config.grocy.base_url}${normalizedEndpoint}`,
87
- method,
88
- headers: { ...config.getCustomHeaders(), ...headers },
89
- body
90
- }
91
- });
92
- }
93
- };
94
- }
@@ -1,16 +0,0 @@
1
- import { systemToolDefinitions } from './definitions.js';
2
- import { SystemToolHandlers } from './handlers.js';
3
- const handlers = new SystemToolHandlers();
4
- export const systemModule = {
5
- definitions: systemToolDefinitions,
6
- handlers: {
7
- // Core System Utilities
8
- system_locations_get: handlers.getLocations,
9
- system_units_get: handlers.getQuantityUnits,
10
- system_users_get: handlers.getUsers,
11
- // Developer Utilities
12
- system_dev_call_api: handlers.callGrocyApi,
13
- system_dev_test_request: handlers.testRequest
14
- }
15
- };
16
- export * from './definitions.js';
@@ -1 +0,0 @@
1
- export {};
@@ -1,36 +0,0 @@
1
- /**
2
- * Common validation utility functions
3
- */
4
- export class ValidationHelpers {
5
- static validateBoolean(value, fieldName) {
6
- if (value !== undefined && typeof value !== 'boolean') {
7
- throw new Error(`${fieldName} must be a boolean`);
8
- }
9
- }
10
- static validateString(value, fieldName) {
11
- if (value !== undefined && typeof value !== 'string') {
12
- throw new Error(`${fieldName} must be a string`);
13
- }
14
- }
15
- static validateNumber(value, fieldName, options) {
16
- if (value !== undefined) {
17
- if (typeof value !== 'number') {
18
- throw new Error(`${fieldName} must be a number`);
19
- }
20
- if (options?.min !== undefined && value < options.min) {
21
- throw new Error(`${fieldName} must be at least ${options.min}`);
22
- }
23
- if (options?.max !== undefined && value > options.max) {
24
- throw new Error(`${fieldName} must be at most ${options.max}`);
25
- }
26
- }
27
- }
28
- static validateKnownOptions(subConfigs, knownOptions, toolName) {
29
- for (const [key] of subConfigs) {
30
- if (!knownOptions.has(key)) {
31
- const validOptions = Array.from(knownOptions).filter(k => k !== 'ack_token').join(', ');
32
- throw new Error(`Unknown sub-configuration option '${key}' for ${toolName} tool. Valid options are: ${validOptions}`);
33
- }
34
- }
35
- }
36
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * Improved type definitions for better type safety
3
- */
4
- import { z } from 'zod';
5
- // Configuration types
6
- export const ConfigSchema = z.object({
7
- env: z.object({
8
- GROCY_API_KEY: z.string().optional(),
9
- RELEASE_VERSION: z.string().optional(),
10
- NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
11
- }),
12
- yaml: z.object({
13
- server: z.object({
14
- enable_http_server: z.boolean().default(false),
15
- http_server_port: z.number().min(1).max(65535).default(8080),
16
- }),
17
- grocy: z.object({
18
- base_url: z.string().url(),
19
- enable_ssl_verify: z.boolean().default(true),
20
- response_size_limit: z.number().positive().default(10000),
21
- }),
22
- tools: z.record(z.string(), z.object({
23
- enabled: z.boolean().default(false),
24
- ack_token: z.string().optional(),
25
- }).catchall(z.unknown())),
26
- })
27
- });
28
- // Validation schemas for runtime type checking
29
- export const ToolResultSchema = z.object({
30
- success: z.boolean(),
31
- data: z.any().optional(),
32
- error: z.string().optional(),
33
- message: z.string().optional(),
34
- details: z.any().optional()
35
- });
36
- export const ToolDefinitionSchema = z.object({
37
- name: z.string(),
38
- description: z.string(),
39
- inputSchema: z.object({
40
- type: z.literal('object'),
41
- properties: z.record(z.any()),
42
- required: z.array(z.string()).optional()
43
- })
44
- });
45
- export const ToolModuleSchema = z.object({
46
- definitions: z.array(ToolDefinitionSchema),
47
- handlers: z.record(z.function())
48
- });
49
- // Type guards
50
- export function isToolResult(obj) {
51
- return ToolResultSchema.safeParse(obj).success;
52
- }
53
- export function isToolDefinition(obj) {
54
- return ToolDefinitionSchema.safeParse(obj).success;
55
- }
56
- export function isToolModule(obj) {
57
- return obj &&
58
- typeof obj === 'object' &&
59
- Array.isArray(obj.definitions) &&
60
- typeof obj.handlers === 'object' &&
61
- obj.definitions.every((def) => isToolDefinition(def));
62
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * Simplified error handling system
3
- */
4
- import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
5
- import { logger } from './logger.js';
6
- /**
7
- * Simplified application error
8
- */
9
- export class AppError extends Error {
10
- category;
11
- statusCode;
12
- operation;
13
- details;
14
- timestamp = new Date();
15
- constructor(message, category, statusCode, operation, details) {
16
- super(message);
17
- this.name = 'AppError';
18
- this.category = category;
19
- this.statusCode = statusCode;
20
- this.operation = operation;
21
- this.details = details;
22
- Object.setPrototypeOf(this, AppError.prototype);
23
- }
24
- toMcpError() {
25
- const errorCode = this.getErrorCode();
26
- return new McpError(errorCode, this.message);
27
- }
28
- getErrorCode() {
29
- switch (this.category) {
30
- case 'VALIDATION': return ErrorCode.InvalidParams;
31
- case 'AUTH': return ErrorCode.InvalidRequest;
32
- case 'API':
33
- if (this.statusCode === 404)
34
- return ErrorCode.InvalidRequest;
35
- if (this.statusCode === 401 || this.statusCode === 403)
36
- return ErrorCode.InvalidRequest;
37
- return ErrorCode.InternalError;
38
- default: return ErrorCode.InternalError;
39
- }
40
- }
41
- }
42
- /**
43
- * API-specific error
44
- */
45
- export class ApiError extends AppError {
46
- constructor(message, statusCode, operation, details) {
47
- super(message, 'API', statusCode, operation, details);
48
- this.name = 'ApiError';
49
- }
50
- static fromAxiosError(error, operation) {
51
- if (error.response) {
52
- return new ApiError(error.response.data?.message || `HTTP ${error.response.status}`, error.response.status, operation, { url: error.config?.url, method: error.config?.method });
53
- }
54
- if (error.request) {
55
- return new ApiError('Network error - unable to reach server', undefined, operation, { url: error.config?.url });
56
- }
57
- return new ApiError(error.message || 'Request error', undefined, operation);
58
- }
59
- }
60
- /**
61
- * Validation error
62
- */
63
- export class ValidationError extends AppError {
64
- constructor(message, operation, details) {
65
- super(message, 'VALIDATION', undefined, operation, details);
66
- this.name = 'ValidationError';
67
- }
68
- }
69
- /**
70
- * Configuration error
71
- */
72
- export class ConfigError extends AppError {
73
- constructor(message, operation, details) {
74
- super(message, 'CONFIG', undefined, operation, details);
75
- this.name = 'ConfigError';
76
- }
77
- }
78
- /**
79
- * Centralized error handler
80
- */
81
- export class ErrorHandler {
82
- /**
83
- * Handle async operations with error wrapping
84
- */
85
- static async handleAsync(operation, context) {
86
- try {
87
- return await operation();
88
- }
89
- catch (error) {
90
- this.logError(error, context);
91
- throw this.wrapError(error, context);
92
- }
93
- }
94
- /**
95
- * Log error with context
96
- */
97
- static logError(error, context) {
98
- const errorMessage = error instanceof Error ? error.message : String(error);
99
- const fullContext = context ? `${context}: ${errorMessage}` : errorMessage;
100
- if (error instanceof AppError) {
101
- logger.warn(fullContext, 'ERROR', {
102
- category: error.category,
103
- statusCode: error.statusCode,
104
- operation: error.operation,
105
- details: error.details
106
- });
107
- }
108
- else {
109
- logger.error(fullContext, 'ERROR', { error });
110
- }
111
- }
112
- /**
113
- * Wrap unknown errors in AppError
114
- */
115
- static wrapError(error, context) {
116
- if (error instanceof AppError) {
117
- return error;
118
- }
119
- if (error instanceof McpError) {
120
- return new AppError(error.message, 'INTERNAL', undefined, context);
121
- }
122
- const message = error instanceof Error ? error.message : String(error);
123
- return new AppError(message, 'INTERNAL', undefined, context);
124
- }
125
- /**
126
- * Convert error to MCP error
127
- */
128
- static toMcpError(error, fallbackMessage = 'Internal error') {
129
- if (error instanceof McpError) {
130
- return error;
131
- }
132
- if (error instanceof AppError) {
133
- return error.toMcpError();
134
- }
135
- const message = error instanceof Error ? error.message : fallbackMessage;
136
- return new McpError(ErrorCode.InternalError, message);
137
- }
138
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * Centralized logging utility with configurable levels and structured output
3
- */
4
- export var LogLevel;
5
- (function (LogLevel) {
6
- LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
7
- LogLevel[LogLevel["WARN"] = 1] = "WARN";
8
- LogLevel[LogLevel["INFO"] = 2] = "INFO";
9
- LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
10
- LogLevel[LogLevel["TRACE"] = 4] = "TRACE";
11
- })(LogLevel || (LogLevel = {}));
12
- export class Logger {
13
- static instance;
14
- logLevel;
15
- enabledCategories = null;
16
- constructor() {
17
- // Default to INFO level, but allow override via environment
18
- this.logLevel = this.parseLogLevel(process.env.LOG_LEVEL || 'INFO');
19
- // Allow filtering by categories
20
- const categories = process.env.LOG_CATEGORIES;
21
- if (categories) {
22
- this.enabledCategories = new Set(categories.split(',').map(c => c.trim().toUpperCase()));
23
- }
24
- }
25
- static getInstance() {
26
- if (!Logger.instance) {
27
- Logger.instance = new Logger();
28
- }
29
- return Logger.instance;
30
- }
31
- parseLogLevel(level) {
32
- const upperLevel = level.toUpperCase();
33
- switch (upperLevel) {
34
- case 'ERROR': return LogLevel.ERROR;
35
- case 'WARN': return LogLevel.WARN;
36
- case 'INFO': return LogLevel.INFO;
37
- case 'DEBUG': return LogLevel.DEBUG;
38
- case 'TRACE': return LogLevel.TRACE;
39
- default: return LogLevel.INFO;
40
- }
41
- }
42
- shouldLog(level, category) {
43
- // Check log level
44
- if (level > this.logLevel) {
45
- return false;
46
- }
47
- // Check category filter
48
- if (this.enabledCategories && category) {
49
- return this.enabledCategories.has(category.toUpperCase());
50
- }
51
- return true;
52
- }
53
- formatMessage(entry) {
54
- const timestamp = entry.timestamp.toISOString();
55
- const level = LogLevel[entry.level].padEnd(5);
56
- const category = entry.category ? `[${entry.category}]` : '';
57
- const data = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
58
- return `${timestamp} ${level} ${category} ${entry.message}${data}`;
59
- }
60
- log(level, message, category, data) {
61
- if (!this.shouldLog(level, category)) {
62
- return;
63
- }
64
- const entry = {
65
- level,
66
- message,
67
- category: category || undefined,
68
- data,
69
- timestamp: new Date()
70
- };
71
- const formattedMessage = this.formatMessage(entry);
72
- // Use stderr for all logs to avoid interfering with MCP stdio protocol
73
- if (level <= LogLevel.ERROR) {
74
- console.error(formattedMessage);
75
- }
76
- else {
77
- console.error(formattedMessage);
78
- }
79
- }
80
- error(message, category, data) {
81
- this.log(LogLevel.ERROR, message, category, data);
82
- }
83
- warn(message, category, data) {
84
- this.log(LogLevel.WARN, message, category, data);
85
- }
86
- info(message, category, data) {
87
- this.log(LogLevel.INFO, message, category, data);
88
- }
89
- debug(message, category, data) {
90
- this.log(LogLevel.DEBUG, message, category, data);
91
- }
92
- trace(message, category, data) {
93
- this.log(LogLevel.TRACE, message, category, data);
94
- }
95
- // Convenience methods for common categories
96
- config(message, data) {
97
- this.info(message, 'CONFIG', data);
98
- }
99
- api(message, data) {
100
- this.debug(message, 'API', data);
101
- }
102
- module(message, data) {
103
- this.debug(message, 'MODULE', data);
104
- }
105
- tools(message, data) {
106
- this.info(message, 'TOOLS', data);
107
- }
108
- server(message, data) {
109
- this.info(message, 'SERVER', data);
110
- }
111
- // Testing utilities
112
- setLogLevel(level) {
113
- this.logLevel = level;
114
- }
115
- setEnabledCategories(categories) {
116
- this.enabledCategories = categories ? new Set(categories.map(c => c.toUpperCase())) : null;
117
- }
118
- }
119
- // Export singleton instance
120
- export const logger = Logger.getInstance();
121
- // Convenience function for quick logging
122
- export function log(level, message, category, data) {
123
- // Access the private log method through the public methods
124
- switch (level) {
125
- case LogLevel.ERROR:
126
- logger.error(message, category, data);
127
- break;
128
- case LogLevel.WARN:
129
- logger.warn(message, category, data);
130
- break;
131
- case LogLevel.INFO:
132
- logger.info(message, category, data);
133
- break;
134
- case LogLevel.DEBUG:
135
- logger.debug(message, category, data);
136
- break;
137
- case LogLevel.TRACE:
138
- logger.trace(message, category, data);
139
- break;
140
- }
141
- }