mcp-grocy 2.7.1 → 2.8.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 +20 -0
- package/README.md +16 -6
- package/build/api/client.js +119 -0
- package/build/config/index.js +283 -0
- package/build/main.js +44 -0
- package/build/resources/CHANGELOG.md +22 -3
- package/build/resources/README.md +16 -6
- package/build/resources/api-reference.md +2 -1
- package/build/resources/examples.md +13 -1
- package/build/resources/response-format.md +1 -3
- package/build/server/http-server.js +398 -0
- package/build/server/mcp-server.js +230 -0
- package/build/server/resources.js +73 -0
- package/build/server/tool-input-zod.js +118 -0
- package/build/tools/base.js +190 -0
- package/build/tools/base.test.js +64 -0
- package/build/tools/household/definitions.js +161 -0
- package/build/tools/household/handlers.js +109 -0
- package/build/tools/household/index.js +25 -0
- package/build/tools/index.js +2 -0
- package/build/tools/inventory/definitions.js +416 -0
- package/build/tools/inventory/handlers.js +443 -0
- package/build/tools/inventory/index.js +32 -0
- package/build/tools/module-loader.js +154 -0
- package/build/tools/recipes/definitions.js +300 -0
- package/build/tools/recipes/handlers.js +533 -0
- package/build/tools/recipes/index.js +34 -0
- package/build/tools/recipes/validations.js +29 -0
- package/build/tools/shopping/definitions.js +147 -0
- package/build/tools/shopping/handlers.js +198 -0
- package/build/tools/shopping/index.js +17 -0
- package/build/tools/system/definitions.js +89 -0
- package/build/tools/system/handlers.js +156 -0
- package/build/tools/system/index.js +16 -0
- package/build/tools/types.js +1 -0
- package/build/tools/validation-helpers.js +39 -0
- package/build/types/index.js +64 -0
- package/build/utils/errors.js +143 -0
- package/build/utils/logger.js +142 -0
- package/build/version.js +1 -1
- package/package.json +27 -25
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { SERVER_NAME } from '../version.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
/** Static docs exposed as MCP resources (used by ResourceHandler and McpServer registration). */
|
|
7
|
+
export const STATIC_MCP_RESOURCE_ENTRIES = [
|
|
8
|
+
{
|
|
9
|
+
slug: 'examples',
|
|
10
|
+
name: 'mcp-grocy usage examples',
|
|
11
|
+
description: 'Examples of calling this MCP server’s tools against Grocy',
|
|
12
|
+
mimeType: 'text/markdown',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
slug: 'response-format',
|
|
16
|
+
name: 'Tool response format',
|
|
17
|
+
description: 'How tool results and system_dev_test_request responses are shaped',
|
|
18
|
+
mimeType: 'text/markdown',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
slug: 'config',
|
|
22
|
+
name: 'Configuration',
|
|
23
|
+
description: 'YAML and environment configuration for mcp-grocy',
|
|
24
|
+
mimeType: 'text/markdown',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
export class ResourceHandler {
|
|
28
|
+
__dirname;
|
|
29
|
+
allowedResources = new Set(STATIC_MCP_RESOURCE_ENTRIES.map((e) => e.slug));
|
|
30
|
+
constructor() {
|
|
31
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
32
|
+
this.__dirname = path.dirname(__filename);
|
|
33
|
+
}
|
|
34
|
+
async listResources() {
|
|
35
|
+
return {
|
|
36
|
+
resources: STATIC_MCP_RESOURCE_ENTRIES.map((e) => ({
|
|
37
|
+
uri: `${SERVER_NAME}://${e.slug}`,
|
|
38
|
+
name: e.name,
|
|
39
|
+
description: e.description,
|
|
40
|
+
mimeType: e.mimeType,
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async readResource(uri) {
|
|
45
|
+
const uriPattern = new RegExp(`^${SERVER_NAME}://(.+)$`);
|
|
46
|
+
const match = uri.match(uriPattern);
|
|
47
|
+
if (!match) {
|
|
48
|
+
throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI format: ${uri}`);
|
|
49
|
+
}
|
|
50
|
+
const resource = match[1];
|
|
51
|
+
if (!resource || !this.allowedResources.has(resource)) {
|
|
52
|
+
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${resource}`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
// In the built app, resources are in build/resources
|
|
56
|
+
// In development, they're in src/resources
|
|
57
|
+
const resourcePath = path.join(this.__dirname, '../resources', `${resource}.md`);
|
|
58
|
+
const content = await fs.promises.readFile(resourcePath, 'utf8');
|
|
59
|
+
return {
|
|
60
|
+
contents: [
|
|
61
|
+
{
|
|
62
|
+
uri,
|
|
63
|
+
mimeType: 'text/markdown',
|
|
64
|
+
text: content,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${resource}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds Zod input schemas from tool definitions (JSON Schema subset used by this repo)
|
|
3
|
+
* so McpServer.registerTool can validate and advertise tools.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
function isStringTupleEnum(values) {
|
|
7
|
+
return values.length > 0 && values.every((v) => typeof v === 'string');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Some MCP clients serialize every tool argument as a string ("12", "true"). Accept those
|
|
11
|
+
* encodings for number/boolean properties: z.preprocess converts them before validation, and the
|
|
12
|
+
* JSON Schema advertised by tools/list still shows the wrapped type because Zod 4's toJSONSchema
|
|
13
|
+
* (io: 'input', which the SDK uses) resolves a preprocess pipe to its output schema — pinned by
|
|
14
|
+
* tests/tool-input-zod.test.ts. Only JSON number syntax and "true"/"false" are converted;
|
|
15
|
+
* anything else is passed through unchanged so the usual type error is still raised.
|
|
16
|
+
*/
|
|
17
|
+
const JSON_NUMBER = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/;
|
|
18
|
+
function numericStringToNumber(value) {
|
|
19
|
+
if (typeof value !== 'string')
|
|
20
|
+
return value;
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (!JSON_NUMBER.test(trimmed))
|
|
23
|
+
return value;
|
|
24
|
+
const parsed = Number(trimmed);
|
|
25
|
+
return Number.isFinite(parsed) ? parsed : value;
|
|
26
|
+
}
|
|
27
|
+
function booleanStringToBoolean(value) {
|
|
28
|
+
if (typeof value !== 'string')
|
|
29
|
+
return value;
|
|
30
|
+
const normalized = value.trim().toLowerCase();
|
|
31
|
+
if (normalized === 'true')
|
|
32
|
+
return true;
|
|
33
|
+
if (normalized === 'false')
|
|
34
|
+
return false;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
const lenientNumber = () => z.preprocess(numericStringToNumber, z.number());
|
|
38
|
+
const lenientBoolean = () => z.preprocess(booleanStringToBoolean, z.boolean());
|
|
39
|
+
function propertyToZod(prop, required) {
|
|
40
|
+
const t = prop.type;
|
|
41
|
+
let inner;
|
|
42
|
+
switch (t) {
|
|
43
|
+
case 'string': {
|
|
44
|
+
if (Array.isArray(prop.enum) && isStringTupleEnum(prop.enum)) {
|
|
45
|
+
inner = z.enum(prop.enum);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
inner = z.string();
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'number':
|
|
53
|
+
case 'integer':
|
|
54
|
+
inner = lenientNumber();
|
|
55
|
+
break;
|
|
56
|
+
case 'boolean':
|
|
57
|
+
inner = lenientBoolean();
|
|
58
|
+
break;
|
|
59
|
+
case 'array': {
|
|
60
|
+
const items = prop.items;
|
|
61
|
+
if (items?.type === 'string' && Array.isArray(items.enum) && isStringTupleEnum(items.enum)) {
|
|
62
|
+
inner = z.array(z.enum(items.enum));
|
|
63
|
+
}
|
|
64
|
+
else if (items?.type === 'string') {
|
|
65
|
+
inner = z.array(z.string());
|
|
66
|
+
}
|
|
67
|
+
else if (items?.type === 'number' || items?.type === 'integer') {
|
|
68
|
+
inner = z.array(lenientNumber());
|
|
69
|
+
}
|
|
70
|
+
else if (items?.type === 'boolean') {
|
|
71
|
+
inner = z.array(lenientBoolean());
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
inner = z.array(z.unknown());
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'object': {
|
|
79
|
+
if (prop.properties && typeof prop.properties === 'object') {
|
|
80
|
+
inner = objectPropsToZod(prop.properties, prop.required);
|
|
81
|
+
}
|
|
82
|
+
else if (prop.additionalProperties &&
|
|
83
|
+
typeof prop.additionalProperties === 'object' &&
|
|
84
|
+
prop.additionalProperties.type === 'string') {
|
|
85
|
+
inner = z.record(z.string(), z.string());
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
inner = z.record(z.string(), z.unknown());
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
inner = z.unknown();
|
|
94
|
+
}
|
|
95
|
+
// Keep the per-property description in the advertised JSON Schema (LLM clients read it).
|
|
96
|
+
const described = prop.description ? inner.describe(prop.description) : inner;
|
|
97
|
+
return required ? described : described.optional();
|
|
98
|
+
}
|
|
99
|
+
function objectPropsToZod(properties, requiredList) {
|
|
100
|
+
const required = new Set(requiredList ?? []);
|
|
101
|
+
const shape = {};
|
|
102
|
+
for (const [key, raw] of Object.entries(properties)) {
|
|
103
|
+
shape[key] = propertyToZod(raw, required.has(key));
|
|
104
|
+
}
|
|
105
|
+
return z.object(shape);
|
|
106
|
+
}
|
|
107
|
+
/** Converts a tool definition's JSON Schema object input into a Zod schema for MCP. */
|
|
108
|
+
export function toolDefinitionInputZod(def) {
|
|
109
|
+
const schema = def.inputSchema;
|
|
110
|
+
if (schema?.type !== 'object') {
|
|
111
|
+
return z.record(z.string(), z.unknown());
|
|
112
|
+
}
|
|
113
|
+
const props = schema.properties;
|
|
114
|
+
if (!props || typeof props !== 'object' || Object.keys(props).length === 0) {
|
|
115
|
+
return z.object({});
|
|
116
|
+
}
|
|
117
|
+
return objectPropsToZod(props, schema.required);
|
|
118
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified base tool handler
|
|
3
|
+
*/
|
|
4
|
+
import apiClient from '../api/client.js';
|
|
5
|
+
import { config } from '../config/index.js';
|
|
6
|
+
import { ErrorHandler, ValidationError } from '../utils/errors.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
export class BaseToolHandler {
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes response data so IDs (like created_object_id or entity id) are numeric.
|
|
11
|
+
*/
|
|
12
|
+
normalizeResponseData(data) {
|
|
13
|
+
if (!data || typeof data !== 'object') {
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(data)) {
|
|
17
|
+
return data.map((item) => this.normalizeResponseData(item));
|
|
18
|
+
}
|
|
19
|
+
if (Object.prototype.toString.call(data) !== '[object Object]') {
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
const normalized = {};
|
|
23
|
+
for (const [key, value] of Object.entries(data)) {
|
|
24
|
+
if (key === 'created_object_id' && value !== null && value !== undefined) {
|
|
25
|
+
const num = Number(value);
|
|
26
|
+
normalized[key] = Number.isInteger(num) && num > 0 ? num : value;
|
|
27
|
+
}
|
|
28
|
+
else if (key === 'id' && typeof value === 'string') {
|
|
29
|
+
const num = Number(value);
|
|
30
|
+
normalized[key] = Number.isInteger(num) && num > 0 ? num : value;
|
|
31
|
+
}
|
|
32
|
+
else if (value && typeof value === 'object') {
|
|
33
|
+
normalized[key] = this.normalizeResponseData(value);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
normalized[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create a standardized success result
|
|
43
|
+
*/
|
|
44
|
+
createSuccess(data, message) {
|
|
45
|
+
const normalizedData = this.normalizeResponseData(data);
|
|
46
|
+
let serializeStructured;
|
|
47
|
+
try {
|
|
48
|
+
serializeStructured = config?.server?.serialize_structured_to_content ?? false;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
serializeStructured = false;
|
|
52
|
+
}
|
|
53
|
+
const textContent = serializeStructured && normalizedData !== undefined && normalizedData !== null
|
|
54
|
+
? message
|
|
55
|
+
? `${message}\n${this.safeStringify(normalizedData)}`
|
|
56
|
+
: this.safeStringify(normalizedData)
|
|
57
|
+
: message || 'Operation completed successfully';
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: textContent,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
structuredContent: {
|
|
66
|
+
data: normalizedData ?? null,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create a standardized error result
|
|
72
|
+
*/
|
|
73
|
+
createError(message, details) {
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: `Error: ${message}`,
|
|
79
|
+
},
|
|
80
|
+
...(details
|
|
81
|
+
? [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: JSON.stringify(details, null, 2),
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
: []),
|
|
88
|
+
],
|
|
89
|
+
isError: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validate required parameters
|
|
94
|
+
*/
|
|
95
|
+
validateRequired(params, required) {
|
|
96
|
+
const missing = required.filter((field) => {
|
|
97
|
+
const value = params[field];
|
|
98
|
+
return value === undefined || value === null || value === '';
|
|
99
|
+
});
|
|
100
|
+
if (missing.length > 0) {
|
|
101
|
+
throw new ValidationError(`Missing required parameters: ${missing.join(', ')}`, 'parameter validation');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Safely make API calls with error handling
|
|
106
|
+
*/
|
|
107
|
+
async apiCall(endpoint, method = 'GET', data, options) {
|
|
108
|
+
return ErrorHandler.handleAsync(async () => {
|
|
109
|
+
const response = await apiClient.request(endpoint, {
|
|
110
|
+
method,
|
|
111
|
+
body: data,
|
|
112
|
+
queryParams: options?.queryParams || {},
|
|
113
|
+
});
|
|
114
|
+
return response.data;
|
|
115
|
+
}, `API ${method} ${endpoint}`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Handle tool execution with standardized error handling
|
|
119
|
+
*/
|
|
120
|
+
async executeToolHandler(handler) {
|
|
121
|
+
try {
|
|
122
|
+
return await handler();
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
ErrorHandler.logError(error, 'tool execution');
|
|
126
|
+
if (error instanceof ValidationError) {
|
|
127
|
+
return this.createError(error.message);
|
|
128
|
+
}
|
|
129
|
+
const message = error instanceof Error ? error.message : 'Internal error';
|
|
130
|
+
return this.createError(`Tool execution failed: ${message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Safe JSON formatting
|
|
135
|
+
*/
|
|
136
|
+
safeStringify(data) {
|
|
137
|
+
try {
|
|
138
|
+
return JSON.stringify(data, null, 2);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logger.warn('Failed to stringify data', 'TOOLS', { error });
|
|
142
|
+
return '[Unable to format data]';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Filter object fields based on allowlist
|
|
147
|
+
*/
|
|
148
|
+
filterFields(objects, fields) {
|
|
149
|
+
return objects.map((obj) => {
|
|
150
|
+
const filtered = {};
|
|
151
|
+
fields.forEach((field) => {
|
|
152
|
+
if (field in obj) {
|
|
153
|
+
filtered[field] = obj[field];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return filtered;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Parse and validate array parameter
|
|
161
|
+
*/
|
|
162
|
+
parseArrayParam(value, paramName) {
|
|
163
|
+
if (!value) {
|
|
164
|
+
throw new ValidationError(`${paramName} is required`);
|
|
165
|
+
}
|
|
166
|
+
if (!Array.isArray(value)) {
|
|
167
|
+
throw new ValidationError(`${paramName} must be an array`);
|
|
168
|
+
}
|
|
169
|
+
if (value.length === 0) {
|
|
170
|
+
throw new ValidationError(`${paramName} cannot be empty`);
|
|
171
|
+
}
|
|
172
|
+
return value.map((v) => String(v));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Parse and validate numeric parameter
|
|
176
|
+
*/
|
|
177
|
+
parseNumberParam(value, paramName, required = true) {
|
|
178
|
+
if (value === undefined || value === null) {
|
|
179
|
+
if (required) {
|
|
180
|
+
throw new ValidationError(`${paramName} is required`);
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const num = Number(value);
|
|
185
|
+
if (isNaN(num)) {
|
|
186
|
+
throw new ValidationError(`${paramName} must be a valid number`);
|
|
187
|
+
}
|
|
188
|
+
return num;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { BaseToolHandler } from './base.js';
|
|
3
|
+
class TestHandler extends BaseToolHandler {
|
|
4
|
+
testNormalize(data) {
|
|
5
|
+
return this.normalizeResponseData(data);
|
|
6
|
+
}
|
|
7
|
+
testSuccess(data, message) {
|
|
8
|
+
return this.createSuccess(data, message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
describe('BaseToolHandler normalization', () => {
|
|
12
|
+
const handler = new TestHandler();
|
|
13
|
+
it('normalizes created_object_id string to number', () => {
|
|
14
|
+
const input = { created_object_id: '1330' };
|
|
15
|
+
const output = handler.testNormalize(input);
|
|
16
|
+
expect(output).toEqual({ created_object_id: 1330 });
|
|
17
|
+
});
|
|
18
|
+
it('leaves created_object_id untouched if already a number', () => {
|
|
19
|
+
const input = { created_object_id: 1330 };
|
|
20
|
+
const output = handler.testNormalize(input);
|
|
21
|
+
expect(output).toEqual({ created_object_id: 1330 });
|
|
22
|
+
});
|
|
23
|
+
it('normalizes string id to number', () => {
|
|
24
|
+
const input = { id: '46', name: 'Arroz redondo' };
|
|
25
|
+
const output = handler.testNormalize(input);
|
|
26
|
+
expect(output).toEqual({ id: 46, name: 'Arroz redondo' });
|
|
27
|
+
});
|
|
28
|
+
it('normalizes nested objects and arrays', () => {
|
|
29
|
+
const input = {
|
|
30
|
+
list: { id: '1', name: 'Groceries' },
|
|
31
|
+
items: [
|
|
32
|
+
{ id: '10', product: { id: '46', name: 'Rice' } },
|
|
33
|
+
{ id: '11', product: { id: '55', name: 'Beans' } },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
const output = handler.testNormalize(input);
|
|
37
|
+
expect(output).toEqual({
|
|
38
|
+
list: { id: 1, name: 'Groceries' },
|
|
39
|
+
items: [
|
|
40
|
+
{ id: 10, product: { id: 46, name: 'Rice' } },
|
|
41
|
+
{ id: 11, product: { id: 55, name: 'Beans' } },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it('does not touch barcodes, dates, or non-id fields', () => {
|
|
46
|
+
const input = {
|
|
47
|
+
id: '1',
|
|
48
|
+
barcode: '012345678905',
|
|
49
|
+
created_at: '2026-09-11 12:00:00',
|
|
50
|
+
note: '46',
|
|
51
|
+
};
|
|
52
|
+
const output = handler.testNormalize(input);
|
|
53
|
+
expect(output).toEqual({
|
|
54
|
+
id: 1,
|
|
55
|
+
barcode: '012345678905',
|
|
56
|
+
created_at: '2026-09-11 12:00:00',
|
|
57
|
+
note: '46',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it('createSuccess automatically applies normalization to structuredContent and textContent', () => {
|
|
61
|
+
const res = handler.testSuccess({ created_object_id: '1330' }, 'Created');
|
|
62
|
+
expect(res.structuredContent?.data).toEqual({ created_object_id: 1330 });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export const householdToolDefinitions = [
|
|
2
|
+
// ==================== CHORE MANAGEMENT ====================
|
|
3
|
+
{
|
|
4
|
+
name: 'household_chores_get',
|
|
5
|
+
description: '[HOUSEHOLD/CHORES] List **Grocy Chores** (recurring household routines: cleaning schedule, maintenance cadence). Not the same as Tasks—use household_tasks_get for one-off to-dos.',
|
|
6
|
+
annotations: { readOnlyHint: true },
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {},
|
|
10
|
+
required: [],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'household_chores_execute',
|
|
15
|
+
description: '[HOUSEHOLD/CHORES] Track the execution of a chore in your Grocy instance.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
choreId: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
description: 'ID of the chore to track execution for. Use household_chores_get tool to find the correct chore ID.',
|
|
22
|
+
},
|
|
23
|
+
executedBy: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
description: 'ID of the user who executed the chore (optional). Use system_users_get tool to find user IDs.',
|
|
26
|
+
},
|
|
27
|
+
trackedTime: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Time when the chore was executed in YYYY-MM-DD HH:mm:ss format (optional, defaults to current time)',
|
|
30
|
+
},
|
|
31
|
+
note: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Optional note for the chore execution',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ['choreId'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
// ==================== TASK MANAGEMENT ====================
|
|
40
|
+
{
|
|
41
|
+
name: 'household_tasks_get',
|
|
42
|
+
description: '[HOUSEHOLD/TASKS] List **Grocy Tasks** (discrete to-dos / task tracker). Not recurring Chores—use household_chores_get for scheduled recurring chores.',
|
|
43
|
+
annotations: { readOnlyHint: true },
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {},
|
|
47
|
+
required: [],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'household_tasks_complete',
|
|
52
|
+
description: '[HOUSEHOLD/TASKS] Mark a task as completed in your Grocy instance.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
taskId: {
|
|
57
|
+
type: 'number',
|
|
58
|
+
description: 'ID of the task to complete. Use household_tasks_get tool to find the correct task ID.',
|
|
59
|
+
},
|
|
60
|
+
note: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Optional note for the task completion',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
required: ['taskId'],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
// ==================== BATTERY MANAGEMENT ====================
|
|
69
|
+
{
|
|
70
|
+
name: 'household_batteries_get',
|
|
71
|
+
description: '[HOUSEHOLD/BATTERIES] Get all batteries from your Grocy instance.',
|
|
72
|
+
annotations: { readOnlyHint: true },
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {},
|
|
76
|
+
required: [],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'household_batteries_charge',
|
|
81
|
+
description: '[HOUSEHOLD/BATTERIES] Track battery charging in your Grocy instance.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
batteryId: {
|
|
86
|
+
type: 'number',
|
|
87
|
+
description: 'ID of the battery to charge. Use household_batteries_get tool to find the correct battery ID.',
|
|
88
|
+
},
|
|
89
|
+
trackedTime: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Time when the battery was charged in YYYY-MM-DD HH:mm:ss format (optional, defaults to current time)',
|
|
92
|
+
},
|
|
93
|
+
note: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Optional note for the battery charge',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
required: ['batteryId'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'household_batteries_print_label',
|
|
103
|
+
description: '[HOUSEHOLD/BATTERIES] Print a Grocycode label for a battery. Use household_batteries_get to find valid batteryId values.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
batteryId: {
|
|
108
|
+
type: 'number',
|
|
109
|
+
description: 'ID of the battery to print label for. Use household_batteries_get tool to find the correct battery ID.',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['batteryId'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
// ==================== CHORE LABEL PRINTING ====================
|
|
116
|
+
{
|
|
117
|
+
name: 'household_chores_print_label',
|
|
118
|
+
description: '[HOUSEHOLD/CHORES] Print a Grocycode label for a chore. Use household_chores_get to find valid choreId values.',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
choreId: {
|
|
123
|
+
type: 'number',
|
|
124
|
+
description: 'ID of the chore to print label for. Use household_chores_get tool to find the correct chore ID.',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ['choreId'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
// ==================== EQUIPMENT MANAGEMENT ====================
|
|
131
|
+
{
|
|
132
|
+
name: 'household_equipment_get',
|
|
133
|
+
description: '[HOUSEHOLD/EQUIPMENT] List **equipment** assets tracked in Grocy (appliances, tools—not chore definitions). For recurring chore definitions use household_chores_get.',
|
|
134
|
+
annotations: { readOnlyHint: true },
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {},
|
|
138
|
+
required: [],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
// ==================== ACTION UTILITIES ====================
|
|
142
|
+
{
|
|
143
|
+
name: 'household_actions_undo',
|
|
144
|
+
description: '[HOUSEHOLD/ACTIONS] Undo a previously executed action (chore execution, task completion, or battery charge).',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
entityType: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
enum: ['chore', 'chores', 'task', 'tasks', 'battery', 'batteries'],
|
|
151
|
+
description: 'Type of entity to undo action for',
|
|
152
|
+
},
|
|
153
|
+
id: {
|
|
154
|
+
type: 'number',
|
|
155
|
+
description: 'ID of the specific execution/completion/charge to undo',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
required: ['entityType', 'id'],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
];
|