mcp-grocy 2.2.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +112 -32
- package/build/api/client.js +11 -13
- package/build/config/index.js +74 -20
- package/build/resources/CHANGELOG.md +31 -0
- package/build/resources/DOCS.md +8 -3
- package/build/resources/README.md +112 -32
- package/build/resources/api-reference.md +88 -88
- package/build/resources/config.md +30 -17
- package/build/resources/examples.md +120 -221
- package/build/resources/installation.md +9 -14
- package/build/resources/response-format.md +18 -13
- package/build/server/http-server.js +154 -42
- package/build/server/mcp-server.js +143 -96
- package/build/server/resources.js +37 -24
- package/build/server/tool-input-zod.js +86 -0
- package/build/tools/base.js +19 -15
- package/build/tools/household/definitions.js +47 -43
- package/build/tools/household/handlers.js +2 -2
- package/build/tools/household/index.js +2 -2
- package/build/tools/inventory/definitions.js +150 -113
- package/build/tools/inventory/handlers.js +55 -42
- package/build/tools/inventory/index.js +2 -2
- package/build/tools/module-loader.js +15 -12
- package/build/tools/recipes/definitions.js +103 -86
- package/build/tools/recipes/handlers.js +44 -38
- package/build/tools/recipes/index.js +3 -3
- package/build/tools/recipes/validations.js +8 -2
- package/build/tools/shopping/definitions.js +22 -20
- package/build/tools/shopping/handlers.js +1 -1
- package/build/tools/shopping/index.js +2 -2
- package/build/tools/system/definitions.js +25 -22
- package/build/tools/system/handlers.js +74 -12
- package/build/tools/system/index.js +2 -2
- package/build/tools/validation-helpers.js +10 -7
- package/build/types/index.js +12 -10
- package/build/utils/errors.js +10 -5
- package/build/utils/logger.js +16 -15
- package/build/version.js +2 -2
- package/package.json +38 -23
|
@@ -2,11 +2,12 @@ export const shoppingToolDefinitions = [
|
|
|
2
2
|
{
|
|
3
3
|
name: 'shopping_list_get',
|
|
4
4
|
description: '[SHOPPING/LIST] Get your current shopping list items.',
|
|
5
|
+
annotations: { readOnlyHint: true },
|
|
5
6
|
inputSchema: {
|
|
6
7
|
type: 'object',
|
|
7
8
|
properties: {},
|
|
8
|
-
required: []
|
|
9
|
-
}
|
|
9
|
+
required: [],
|
|
10
|
+
},
|
|
10
11
|
},
|
|
11
12
|
{
|
|
12
13
|
name: 'shopping_list_add_item',
|
|
@@ -16,25 +17,25 @@ export const shoppingToolDefinitions = [
|
|
|
16
17
|
properties: {
|
|
17
18
|
productId: {
|
|
18
19
|
type: 'number',
|
|
19
|
-
description: 'ID of the product to add. Use inventory_products_get tool to find the correct product ID by searching for the product name in the results.'
|
|
20
|
+
description: 'ID of the product to add. Use inventory_products_get tool to find the correct product ID by searching for the product name in the results.',
|
|
20
21
|
},
|
|
21
22
|
amount: {
|
|
22
23
|
type: 'number',
|
|
23
|
-
description:
|
|
24
|
-
default: 1
|
|
24
|
+
description: "Amount to add to shopping list in the product's stock unit (e.g., 2 pieces, 1.5 kg, 750 ml). Ensure you know the product's unit before specifying amount. Default: 1",
|
|
25
|
+
default: 1,
|
|
25
26
|
},
|
|
26
27
|
shoppingListId: {
|
|
27
28
|
type: 'number',
|
|
28
29
|
description: 'ID of the shopping list to add to (default: 1). Most users have only one shopping list with ID 1.',
|
|
29
|
-
default: 1
|
|
30
|
+
default: 1,
|
|
30
31
|
},
|
|
31
32
|
note: {
|
|
32
33
|
type: 'string',
|
|
33
|
-
description: 'Optional note for the shopping list item'
|
|
34
|
-
}
|
|
34
|
+
description: 'Optional note for the shopping list item',
|
|
35
|
+
},
|
|
35
36
|
},
|
|
36
|
-
required: ['productId']
|
|
37
|
-
}
|
|
37
|
+
required: ['productId'],
|
|
38
|
+
},
|
|
38
39
|
},
|
|
39
40
|
{
|
|
40
41
|
name: 'shopping_list_remove_item',
|
|
@@ -44,20 +45,21 @@ export const shoppingToolDefinitions = [
|
|
|
44
45
|
properties: {
|
|
45
46
|
shoppingListItemId: {
|
|
46
47
|
type: 'number',
|
|
47
|
-
description: 'ID of the shopping list item to remove. Use shopping_list_get tool to find the correct shopping list item ID by looking at the "id" field in the results.'
|
|
48
|
-
}
|
|
48
|
+
description: 'ID of the shopping list item to remove. Use shopping_list_get tool to find the correct shopping list item ID by looking at the "id" field in the results.',
|
|
49
|
+
},
|
|
49
50
|
},
|
|
50
|
-
required: ['shoppingListItemId']
|
|
51
|
-
}
|
|
51
|
+
required: ['shoppingListItemId'],
|
|
52
|
+
},
|
|
52
53
|
},
|
|
53
54
|
{
|
|
54
55
|
name: 'shopping_locations_get',
|
|
55
|
-
description: '[SHOPPING/LOCATIONS] Get
|
|
56
|
+
description: '[SHOPPING/LOCATIONS] Get **retail store / shop** locations where you buy groceries (Grocy shopping locations). NOT pantry or home storage—use system_locations_get for storage location IDs (locationId). Use shopping_locations_get for storeId when adding shopping-list items or store-specific workflows.',
|
|
57
|
+
annotations: { readOnlyHint: true },
|
|
56
58
|
inputSchema: {
|
|
57
59
|
type: 'object',
|
|
58
60
|
properties: {},
|
|
59
|
-
required: []
|
|
60
|
-
}
|
|
61
|
+
required: [],
|
|
62
|
+
},
|
|
61
63
|
},
|
|
62
64
|
{
|
|
63
65
|
name: 'shopping_list_print_thermal',
|
|
@@ -65,7 +67,7 @@ export const shoppingToolDefinitions = [
|
|
|
65
67
|
inputSchema: {
|
|
66
68
|
type: 'object',
|
|
67
69
|
properties: {},
|
|
68
|
-
required: []
|
|
69
|
-
}
|
|
70
|
-
}
|
|
70
|
+
required: [],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
71
73
|
];
|
|
@@ -14,7 +14,7 @@ export class ShoppingToolHandlers extends BaseToolHandler {
|
|
|
14
14
|
product_id: productId,
|
|
15
15
|
amount,
|
|
16
16
|
shopping_list_id: shoppingListId,
|
|
17
|
-
note
|
|
17
|
+
note,
|
|
18
18
|
};
|
|
19
19
|
const result = await this.apiCall('/objects/shopping_list', 'POST', body);
|
|
20
20
|
return this.createSuccess(result, 'Shopping list item added successfully');
|
|
@@ -8,7 +8,7 @@ export const shoppingModule = {
|
|
|
8
8
|
shopping_list_add_item: handlers.addShoppingListItem,
|
|
9
9
|
shopping_list_remove_item: handlers.removeShoppingListItem,
|
|
10
10
|
shopping_locations_get: handlers.getShoppingLocations,
|
|
11
|
-
shopping_list_print_thermal: handlers.printShoppingListThermal
|
|
12
|
-
}
|
|
11
|
+
shopping_list_print_thermal: handlers.printShoppingListThermal,
|
|
12
|
+
},
|
|
13
13
|
};
|
|
14
14
|
export * from './definitions.js';
|
|
@@ -3,30 +3,33 @@ export const systemToolDefinitions = [
|
|
|
3
3
|
// ==================== CORE SYSTEM UTILITIES ====================
|
|
4
4
|
{
|
|
5
5
|
name: 'system_locations_get',
|
|
6
|
-
description: '[SYSTEM/LOCATIONS] Get
|
|
6
|
+
description: '[SYSTEM/LOCATIONS] Get **home storage / stock** locations (freezer, pantry, fridge—where inventory lives). NOT retail stores—use shopping_locations_get for shop/store IDs (storeId). Use this for locationId on stock, transfers, and purchases.',
|
|
7
|
+
annotations: { readOnlyHint: true },
|
|
7
8
|
inputSchema: {
|
|
8
9
|
type: 'object',
|
|
9
10
|
properties: {},
|
|
10
|
-
required: []
|
|
11
|
-
}
|
|
11
|
+
required: [],
|
|
12
|
+
},
|
|
12
13
|
},
|
|
13
14
|
{
|
|
14
15
|
name: 'system_units_get',
|
|
15
16
|
description: '[SYSTEM/UNITS] Get all quantity units from your Grocy instance.',
|
|
17
|
+
annotations: { readOnlyHint: true },
|
|
16
18
|
inputSchema: {
|
|
17
19
|
type: 'object',
|
|
18
20
|
properties: {},
|
|
19
|
-
required: []
|
|
20
|
-
}
|
|
21
|
+
required: [],
|
|
22
|
+
},
|
|
21
23
|
},
|
|
22
24
|
{
|
|
23
25
|
name: 'system_users_get',
|
|
24
26
|
description: '[SYSTEM/USERS] Get all users from your Grocy instance.',
|
|
27
|
+
annotations: { readOnlyHint: true },
|
|
25
28
|
inputSchema: {
|
|
26
29
|
type: 'object',
|
|
27
30
|
properties: {},
|
|
28
|
-
required: []
|
|
29
|
-
}
|
|
31
|
+
required: [],
|
|
32
|
+
},
|
|
30
33
|
},
|
|
31
34
|
// ==================== DEVELOPER UTILITIES ====================
|
|
32
35
|
{
|
|
@@ -37,21 +40,21 @@ export const systemToolDefinitions = [
|
|
|
37
40
|
properties: {
|
|
38
41
|
endpoint: {
|
|
39
42
|
type: 'string',
|
|
40
|
-
description: 'Grocy API endpoint to call (e.g., "objects/products"). Do not include /api/ prefix.'
|
|
43
|
+
description: 'Grocy API endpoint to call (e.g., "objects/products"). Do not include /api/ prefix.',
|
|
41
44
|
},
|
|
42
45
|
method: {
|
|
43
46
|
type: 'string',
|
|
44
47
|
enum: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
45
48
|
description: 'HTTP method to use',
|
|
46
|
-
default: 'GET'
|
|
49
|
+
default: 'GET',
|
|
47
50
|
},
|
|
48
51
|
body: {
|
|
49
52
|
type: 'object',
|
|
50
|
-
description: 'Optional request body for POST/PUT requests'
|
|
51
|
-
}
|
|
53
|
+
description: 'Optional request body for POST/PUT requests',
|
|
54
|
+
},
|
|
52
55
|
},
|
|
53
|
-
required: ['endpoint']
|
|
54
|
-
}
|
|
56
|
+
required: ['endpoint'],
|
|
57
|
+
},
|
|
55
58
|
},
|
|
56
59
|
{
|
|
57
60
|
name: 'system_dev_test_request',
|
|
@@ -62,25 +65,25 @@ export const systemToolDefinitions = [
|
|
|
62
65
|
method: {
|
|
63
66
|
type: 'string',
|
|
64
67
|
enum: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
65
|
-
description: 'HTTP method to use'
|
|
68
|
+
description: 'HTTP method to use',
|
|
66
69
|
},
|
|
67
70
|
endpoint: {
|
|
68
71
|
type: 'string',
|
|
69
|
-
description: 'Endpoint path (e.g. "/users"). Do not include full URLs - only the path.'
|
|
72
|
+
description: 'Endpoint path (e.g. "/users"). Do not include full URLs - only the path.',
|
|
70
73
|
},
|
|
71
74
|
body: {
|
|
72
75
|
type: 'object',
|
|
73
|
-
description: 'Optional request body for POST/PUT requests'
|
|
76
|
+
description: 'Optional request body for POST/PUT requests',
|
|
74
77
|
},
|
|
75
78
|
headers: {
|
|
76
79
|
type: 'object',
|
|
77
80
|
description: 'Optional request headers for one-time use.',
|
|
78
81
|
additionalProperties: {
|
|
79
|
-
type: 'string'
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
+
type: 'string',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
82
85
|
},
|
|
83
|
-
required: ['method', 'endpoint']
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
+
required: ['method', 'endpoint'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
86
89
|
];
|
|
@@ -4,6 +4,64 @@ import apiClient from '../../api/client.js';
|
|
|
4
4
|
import { config } from '../../config/index.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
6
|
export class SystemToolHandlers extends BaseToolHandler {
|
|
7
|
+
redactHeaders(headers) {
|
|
8
|
+
const redacted = { ...headers };
|
|
9
|
+
const sensitiveHeaders = new Set([
|
|
10
|
+
'grocy-api-key',
|
|
11
|
+
'authorization',
|
|
12
|
+
'x-api-key',
|
|
13
|
+
'proxy-authorization',
|
|
14
|
+
]);
|
|
15
|
+
for (const key of Object.keys(redacted)) {
|
|
16
|
+
if (sensitiveHeaders.has(key.toLowerCase())) {
|
|
17
|
+
redacted[key] = '[REDACTED]';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return redacted;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Truncate a string to at most `maxBytes` UTF-8 bytes without splitting a codepoint.
|
|
24
|
+
*/
|
|
25
|
+
truncateStringToUtf8Bytes(str, maxBytes) {
|
|
26
|
+
const buf = Buffer.from(str, 'utf8');
|
|
27
|
+
if (buf.length <= maxBytes) {
|
|
28
|
+
return { text: str, byteLength: buf.length };
|
|
29
|
+
}
|
|
30
|
+
let end = maxBytes;
|
|
31
|
+
while (end > 0) {
|
|
32
|
+
while (end > 0 && (buf[end - 1] & 0xc0) === 0x80) {
|
|
33
|
+
end--;
|
|
34
|
+
}
|
|
35
|
+
if (end === 0) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
const text = buf.subarray(0, end).toString('utf8');
|
|
39
|
+
if (Buffer.byteLength(text, 'utf8') === end) {
|
|
40
|
+
return { text, byteLength: end };
|
|
41
|
+
}
|
|
42
|
+
end--;
|
|
43
|
+
}
|
|
44
|
+
return { text: '', byteLength: 0 };
|
|
45
|
+
}
|
|
46
|
+
applyResponseSizeLimit(body) {
|
|
47
|
+
const sizeLimit = config.grocy.response_size_limit;
|
|
48
|
+
const jsonBody = JSON.stringify(body);
|
|
49
|
+
const originalSize = Buffer.byteLength(jsonBody, 'utf8');
|
|
50
|
+
if (originalSize <= sizeLimit) {
|
|
51
|
+
return { body };
|
|
52
|
+
}
|
|
53
|
+
const { text: truncatedJsonBody, byteLength: prefixBytes } = this.truncateStringToUtf8Bytes(jsonBody, sizeLimit);
|
|
54
|
+
const returnedSize = Buffer.byteLength(truncatedJsonBody, 'utf8');
|
|
55
|
+
return {
|
|
56
|
+
body: `${truncatedJsonBody}...[TRUNCATED]`,
|
|
57
|
+
truncated: {
|
|
58
|
+
originalSize,
|
|
59
|
+
returnedSize,
|
|
60
|
+
truncationPoint: prefixBytes,
|
|
61
|
+
sizeLimit,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
7
65
|
// ==================== CORE SYSTEM UTILITIES ====================
|
|
8
66
|
getLocations = async () => {
|
|
9
67
|
return this.executeToolHandler(async () => {
|
|
@@ -34,7 +92,7 @@ export class SystemToolHandlers extends BaseToolHandler {
|
|
|
34
92
|
try {
|
|
35
93
|
const response = await apiClient.request(`/${cleanEndpoint}`, {
|
|
36
94
|
method,
|
|
37
|
-
body
|
|
95
|
+
body,
|
|
38
96
|
});
|
|
39
97
|
return this.createSuccess(response.data);
|
|
40
98
|
}
|
|
@@ -49,34 +107,38 @@ export class SystemToolHandlers extends BaseToolHandler {
|
|
|
49
107
|
throw new McpError(ErrorCode.InvalidParams, 'method and endpoint are required');
|
|
50
108
|
}
|
|
51
109
|
const normalizedEndpoint = `/${endpoint.replace(/^\/+|\/+$/g, '')}`;
|
|
110
|
+
const requestHeaders = { ...config.getCustomHeaders(), ...headers };
|
|
111
|
+
const safeRequestHeaders = this.redactHeaders(requestHeaders);
|
|
52
112
|
try {
|
|
53
113
|
const startTime = Date.now();
|
|
54
114
|
const response = await apiClient.request(normalizedEndpoint, {
|
|
55
115
|
method,
|
|
56
116
|
body,
|
|
57
|
-
headers:
|
|
117
|
+
headers: requestHeaders,
|
|
58
118
|
});
|
|
59
119
|
const endTime = Date.now();
|
|
120
|
+
const responseWithLimit = this.applyResponseSizeLimit(response.data);
|
|
60
121
|
const responseObj = {
|
|
61
122
|
request: {
|
|
62
123
|
url: `${config.grocy.base_url}${normalizedEndpoint}`,
|
|
63
124
|
method,
|
|
64
|
-
headers:
|
|
125
|
+
headers: safeRequestHeaders,
|
|
65
126
|
body,
|
|
66
|
-
authMethod: config.grocy.api_key ? 'apikey' : 'none'
|
|
127
|
+
authMethod: config.grocy.api_key ? 'apikey' : 'none',
|
|
67
128
|
},
|
|
68
129
|
response: {
|
|
69
130
|
statusCode: response.status,
|
|
70
131
|
timing: `${endTime - startTime}ms`,
|
|
71
132
|
headers: response.headers,
|
|
72
|
-
body:
|
|
133
|
+
body: responseWithLimit.body,
|
|
73
134
|
},
|
|
74
135
|
validation: {
|
|
75
136
|
isError: response.status >= 400,
|
|
76
|
-
messages: response.status >= 400
|
|
77
|
-
[`Request failed with status ${response.status}`]
|
|
78
|
-
['Request completed successfully']
|
|
79
|
-
|
|
137
|
+
messages: response.status >= 400
|
|
138
|
+
? [`Request failed with status ${response.status}`]
|
|
139
|
+
: ['Request completed successfully'],
|
|
140
|
+
...(responseWithLimit.truncated ? { truncated: responseWithLimit.truncated } : {}),
|
|
141
|
+
},
|
|
80
142
|
};
|
|
81
143
|
return this.createSuccess(responseObj);
|
|
82
144
|
}
|
|
@@ -85,9 +147,9 @@ export class SystemToolHandlers extends BaseToolHandler {
|
|
|
85
147
|
request: {
|
|
86
148
|
url: `${config.grocy.base_url}${normalizedEndpoint}`,
|
|
87
149
|
method,
|
|
88
|
-
headers:
|
|
89
|
-
body
|
|
90
|
-
}
|
|
150
|
+
headers: safeRequestHeaders,
|
|
151
|
+
body,
|
|
152
|
+
},
|
|
91
153
|
});
|
|
92
154
|
}
|
|
93
155
|
};
|
|
@@ -10,7 +10,7 @@ export const systemModule = {
|
|
|
10
10
|
system_users_get: handlers.getUsers,
|
|
11
11
|
// Developer Utilities
|
|
12
12
|
system_dev_call_api: handlers.callGrocyApi,
|
|
13
|
-
system_dev_test_request: handlers.testRequest
|
|
14
|
-
}
|
|
13
|
+
system_dev_test_request: handlers.testRequest,
|
|
14
|
+
},
|
|
15
15
|
};
|
|
16
16
|
export * from './definitions.js';
|
|
@@ -1,35 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Common validation utility functions
|
|
3
3
|
*/
|
|
4
|
+
import { ValidationError } from '../utils/errors.js';
|
|
4
5
|
export class ValidationHelpers {
|
|
5
6
|
static validateBoolean(value, fieldName) {
|
|
6
7
|
if (value !== undefined && typeof value !== 'boolean') {
|
|
7
|
-
throw new
|
|
8
|
+
throw new ValidationError(`${fieldName} must be a boolean`, 'sub-config validation');
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
static validateString(value, fieldName) {
|
|
11
12
|
if (value !== undefined && typeof value !== 'string') {
|
|
12
|
-
throw new
|
|
13
|
+
throw new ValidationError(`${fieldName} must be a string`, 'sub-config validation');
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
static validateNumber(value, fieldName, options) {
|
|
16
17
|
if (value !== undefined) {
|
|
17
18
|
if (typeof value !== 'number') {
|
|
18
|
-
throw new
|
|
19
|
+
throw new ValidationError(`${fieldName} must be a number`, 'sub-config validation');
|
|
19
20
|
}
|
|
20
21
|
if (options?.min !== undefined && value < options.min) {
|
|
21
|
-
throw new
|
|
22
|
+
throw new ValidationError(`${fieldName} must be at least ${options.min}`, 'sub-config validation');
|
|
22
23
|
}
|
|
23
24
|
if (options?.max !== undefined && value > options.max) {
|
|
24
|
-
throw new
|
|
25
|
+
throw new ValidationError(`${fieldName} must be at most ${options.max}`, 'sub-config validation');
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
static validateKnownOptions(subConfigs, knownOptions, toolName) {
|
|
29
30
|
for (const [key] of subConfigs) {
|
|
30
31
|
if (!knownOptions.has(key)) {
|
|
31
|
-
const validOptions = Array.from(knownOptions)
|
|
32
|
-
|
|
32
|
+
const validOptions = Array.from(knownOptions)
|
|
33
|
+
.filter((k) => k !== 'ack_token')
|
|
34
|
+
.join(', ');
|
|
35
|
+
throw new ValidationError(`Unknown sub-configuration option '${key}' for ${toolName} tool. Valid options are: ${validOptions}`, 'sub-config validation');
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
}
|
package/build/types/index.js
CHANGED
|
@@ -19,11 +19,13 @@ export const ConfigSchema = z.object({
|
|
|
19
19
|
enable_ssl_verify: z.boolean().default(true),
|
|
20
20
|
response_size_limit: z.number().positive().default(10000),
|
|
21
21
|
}),
|
|
22
|
-
tools: z.record(z.string(), z
|
|
22
|
+
tools: z.record(z.string(), z
|
|
23
|
+
.object({
|
|
23
24
|
enabled: z.boolean().default(false),
|
|
24
25
|
ack_token: z.string().optional(),
|
|
25
|
-
})
|
|
26
|
-
|
|
26
|
+
})
|
|
27
|
+
.catchall(z.unknown())),
|
|
28
|
+
}),
|
|
27
29
|
});
|
|
28
30
|
// Validation schemas for runtime type checking
|
|
29
31
|
export const ToolResultSchema = z.object({
|
|
@@ -31,20 +33,20 @@ export const ToolResultSchema = z.object({
|
|
|
31
33
|
data: z.any().optional(),
|
|
32
34
|
error: z.string().optional(),
|
|
33
35
|
message: z.string().optional(),
|
|
34
|
-
details: z.any().optional()
|
|
36
|
+
details: z.any().optional(),
|
|
35
37
|
});
|
|
36
38
|
export const ToolDefinitionSchema = z.object({
|
|
37
39
|
name: z.string(),
|
|
38
40
|
description: z.string(),
|
|
39
41
|
inputSchema: z.object({
|
|
40
42
|
type: z.literal('object'),
|
|
41
|
-
properties: z.record(z.any()),
|
|
42
|
-
required: z.array(z.string()).optional()
|
|
43
|
-
})
|
|
43
|
+
properties: z.record(z.string(), z.any()),
|
|
44
|
+
required: z.array(z.string()).optional(),
|
|
45
|
+
}),
|
|
44
46
|
});
|
|
45
47
|
export const ToolModuleSchema = z.object({
|
|
46
48
|
definitions: z.array(ToolDefinitionSchema),
|
|
47
|
-
handlers: z.record(z.
|
|
49
|
+
handlers: z.record(z.string(), z.custom((v) => typeof v === 'function')),
|
|
48
50
|
});
|
|
49
51
|
// Type guards
|
|
50
52
|
export function isToolResult(obj) {
|
|
@@ -54,9 +56,9 @@ export function isToolDefinition(obj) {
|
|
|
54
56
|
return ToolDefinitionSchema.safeParse(obj).success;
|
|
55
57
|
}
|
|
56
58
|
export function isToolModule(obj) {
|
|
57
|
-
return obj &&
|
|
59
|
+
return (obj &&
|
|
58
60
|
typeof obj === 'object' &&
|
|
59
61
|
Array.isArray(obj.definitions) &&
|
|
60
62
|
typeof obj.handlers === 'object' &&
|
|
61
|
-
obj.definitions.every((def) => isToolDefinition(def));
|
|
63
|
+
obj.definitions.every((def) => isToolDefinition(def)));
|
|
62
64
|
}
|
package/build/utils/errors.js
CHANGED
|
@@ -27,15 +27,18 @@ export class AppError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
getErrorCode() {
|
|
29
29
|
switch (this.category) {
|
|
30
|
-
case 'VALIDATION':
|
|
31
|
-
|
|
30
|
+
case 'VALIDATION':
|
|
31
|
+
return ErrorCode.InvalidParams;
|
|
32
|
+
case 'AUTH':
|
|
33
|
+
return ErrorCode.InvalidRequest;
|
|
32
34
|
case 'API':
|
|
33
35
|
if (this.statusCode === 404)
|
|
34
36
|
return ErrorCode.InvalidRequest;
|
|
35
37
|
if (this.statusCode === 401 || this.statusCode === 403)
|
|
36
38
|
return ErrorCode.InvalidRequest;
|
|
37
39
|
return ErrorCode.InternalError;
|
|
38
|
-
default:
|
|
40
|
+
default:
|
|
41
|
+
return ErrorCode.InternalError;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -52,7 +55,9 @@ export class ApiError extends AppError {
|
|
|
52
55
|
return new ApiError(error.response.data?.message || `HTTP ${error.response.status}`, error.response.status, operation, { url: error.config?.url, method: error.config?.method });
|
|
53
56
|
}
|
|
54
57
|
if (error.request) {
|
|
55
|
-
return new ApiError('Network error - unable to reach server', undefined, operation, {
|
|
58
|
+
return new ApiError('Network error - unable to reach server', undefined, operation, {
|
|
59
|
+
url: error.config?.url,
|
|
60
|
+
});
|
|
56
61
|
}
|
|
57
62
|
return new ApiError(error.message || 'Request error', undefined, operation);
|
|
58
63
|
}
|
|
@@ -102,7 +107,7 @@ export class ErrorHandler {
|
|
|
102
107
|
category: error.category,
|
|
103
108
|
statusCode: error.statusCode,
|
|
104
109
|
operation: error.operation,
|
|
105
|
-
details: error.details
|
|
110
|
+
details: error.details,
|
|
106
111
|
});
|
|
107
112
|
}
|
|
108
113
|
else {
|
package/build/utils/logger.js
CHANGED
|
@@ -19,7 +19,7 @@ export class Logger {
|
|
|
19
19
|
// Allow filtering by categories
|
|
20
20
|
const categories = process.env.LOG_CATEGORIES;
|
|
21
21
|
if (categories) {
|
|
22
|
-
this.enabledCategories = new Set(categories.split(',').map(c => c.trim().toUpperCase()));
|
|
22
|
+
this.enabledCategories = new Set(categories.split(',').map((c) => c.trim().toUpperCase()));
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
static getInstance() {
|
|
@@ -31,12 +31,18 @@ export class Logger {
|
|
|
31
31
|
parseLogLevel(level) {
|
|
32
32
|
const upperLevel = level.toUpperCase();
|
|
33
33
|
switch (upperLevel) {
|
|
34
|
-
case 'ERROR':
|
|
35
|
-
|
|
36
|
-
case '
|
|
37
|
-
|
|
38
|
-
case '
|
|
39
|
-
|
|
34
|
+
case 'ERROR':
|
|
35
|
+
return LogLevel.ERROR;
|
|
36
|
+
case 'WARN':
|
|
37
|
+
return LogLevel.WARN;
|
|
38
|
+
case 'INFO':
|
|
39
|
+
return LogLevel.INFO;
|
|
40
|
+
case 'DEBUG':
|
|
41
|
+
return LogLevel.DEBUG;
|
|
42
|
+
case 'TRACE':
|
|
43
|
+
return LogLevel.TRACE;
|
|
44
|
+
default:
|
|
45
|
+
return LogLevel.INFO;
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
shouldLog(level, category) {
|
|
@@ -66,16 +72,11 @@ export class Logger {
|
|
|
66
72
|
message,
|
|
67
73
|
category: category || undefined,
|
|
68
74
|
data,
|
|
69
|
-
timestamp: new Date()
|
|
75
|
+
timestamp: new Date(),
|
|
70
76
|
};
|
|
71
77
|
const formattedMessage = this.formatMessage(entry);
|
|
72
78
|
// Use stderr for all logs to avoid interfering with MCP stdio protocol
|
|
73
|
-
|
|
74
|
-
console.error(formattedMessage);
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
console.error(formattedMessage);
|
|
78
|
-
}
|
|
79
|
+
console.error(formattedMessage);
|
|
79
80
|
}
|
|
80
81
|
error(message, category, data) {
|
|
81
82
|
this.log(LogLevel.ERROR, message, category, data);
|
|
@@ -113,7 +114,7 @@ export class Logger {
|
|
|
113
114
|
this.logLevel = level;
|
|
114
115
|
}
|
|
115
116
|
setEnabledCategories(categories) {
|
|
116
|
-
this.enabledCategories = categories ? new Set(categories.map(c => c.toUpperCase())) : null;
|
|
117
|
+
this.enabledCategories = categories ? new Set(categories.map((c) => c.toUpperCase())) : null;
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
// Export singleton instance
|
package/build/version.js
CHANGED