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
|
@@ -1,62 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Reduced complexity and improved performance
|
|
2
|
+
* Grocy MCP server — McpServer API (registerTool / registerResource).
|
|
4
3
|
*/
|
|
5
|
-
import {
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
import { CallToolRequestSchema, ErrorCode,
|
|
8
|
-
import {
|
|
9
|
-
import { VERSION, PACKAGE_NAME as SERVER_NAME } from '../version.js';
|
|
6
|
+
import { CallToolRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { VERSION, PACKAGE_NAME, SERVER_NAME as RESOURCE_URI_SCHEME } from '../version.js';
|
|
10
8
|
import { createToolRegistry } from '../tools/index.js';
|
|
11
9
|
import { config } from '../config/index.js';
|
|
12
10
|
import { startHttpServer } from './http-server.js';
|
|
13
|
-
import { ResourceHandler } from './resources.js';
|
|
11
|
+
import { ResourceHandler, STATIC_MCP_RESOURCE_ENTRIES } from './resources.js';
|
|
12
|
+
import { toolDefinitionInputZod } from './tool-input-zod.js';
|
|
14
13
|
import { logger } from '../utils/logger.js';
|
|
15
14
|
import { ErrorHandler } from '../utils/errors.js';
|
|
15
|
+
/** Janix mcp-validator (2025-03-26) checks `tools.asyncSupported`; merge keeps this when SDK adds `listChanged`. */
|
|
16
|
+
const GROCY_SERVER_CAPABILITIES = {
|
|
17
|
+
tools: {
|
|
18
|
+
asyncSupported: true,
|
|
19
|
+
},
|
|
20
|
+
resources: {},
|
|
21
|
+
prompts: {},
|
|
22
|
+
};
|
|
23
|
+
const SERVER_INFO = {
|
|
24
|
+
name: PACKAGE_NAME,
|
|
25
|
+
version: VERSION,
|
|
26
|
+
websiteUrl: 'https://github.com/miguelangel-nubla/mcp-grocy',
|
|
27
|
+
description: 'MCP server for Grocy. Documentation: https://github.com/miguelangel-nubla/mcp-grocy/blob/main/README.md',
|
|
28
|
+
};
|
|
16
29
|
export class GrocyMcpServer {
|
|
17
|
-
|
|
30
|
+
static sigintHandlerRegistered = false;
|
|
31
|
+
static activeServers = new Set();
|
|
32
|
+
mcp;
|
|
18
33
|
enabledTools = new Set();
|
|
19
34
|
toolSubConfigs = new Map();
|
|
20
35
|
toolAckTokens = new Map();
|
|
21
36
|
resourceHandler;
|
|
22
37
|
toolRegistry;
|
|
23
|
-
constructor(
|
|
24
|
-
this.
|
|
38
|
+
constructor(mcp, toolRegistry, resourceHandler) {
|
|
39
|
+
this.mcp = mcp;
|
|
25
40
|
this.toolRegistry = toolRegistry;
|
|
26
41
|
this.resourceHandler = resourceHandler;
|
|
27
42
|
this.parseToolConfiguration();
|
|
28
|
-
this.
|
|
29
|
-
this.setupErrorHandling();
|
|
43
|
+
this.registerToolsAndResources(this.mcp);
|
|
44
|
+
this.setupErrorHandling(this.mcp);
|
|
30
45
|
}
|
|
31
46
|
static async create() {
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
]);
|
|
37
|
-
// Create server
|
|
38
|
-
const server = new Server({
|
|
39
|
-
name: SERVER_NAME,
|
|
40
|
-
version: VERSION,
|
|
41
|
-
serverUrl: "https://github.com/miguelangel-nubla/mcp-grocy",
|
|
42
|
-
documentationUrl: "https://github.com/miguelangel-nubla/mcp-grocy/blob/main/README.md"
|
|
43
|
-
}, {
|
|
44
|
-
capabilities: {
|
|
45
|
-
tools: {},
|
|
46
|
-
resources: {},
|
|
47
|
-
prompts: {}
|
|
48
|
-
}
|
|
47
|
+
const toolRegistry = await createToolRegistry();
|
|
48
|
+
const resourceHandler = new ResourceHandler();
|
|
49
|
+
const mcp = new McpServer(SERVER_INFO, {
|
|
50
|
+
capabilities: GROCY_SERVER_CAPABILITIES,
|
|
49
51
|
});
|
|
50
|
-
|
|
52
|
+
GrocyMcpServer.activeServers.add(mcp);
|
|
53
|
+
return new GrocyMcpServer(mcp, toolRegistry, resourceHandler);
|
|
51
54
|
}
|
|
52
55
|
parseToolConfiguration() {
|
|
53
56
|
const { enabledTools, toolSubConfigs, toolAckTokens } = config.parseToolConfiguration();
|
|
54
57
|
this.toolSubConfigs = toolSubConfigs;
|
|
55
58
|
this.toolAckTokens = toolAckTokens;
|
|
56
|
-
// Validate tool names
|
|
57
59
|
const validToolNames = new Set(this.toolRegistry.getToolNames());
|
|
58
60
|
if (enabledTools.size > 0) {
|
|
59
|
-
const invalidTools = Array.from(enabledTools).filter(tool => !validToolNames.has(tool));
|
|
61
|
+
const invalidTools = Array.from(enabledTools).filter((tool) => !validToolNames.has(tool));
|
|
60
62
|
if (invalidTools.length > 0) {
|
|
61
63
|
const validNames = Array.from(validToolNames).sort().join(', ');
|
|
62
64
|
logger.error(`Invalid tools: ${invalidTools.join(', ')}. Valid: ${validNames}`, 'CONFIG');
|
|
@@ -69,84 +71,130 @@ export class GrocyMcpServer {
|
|
|
69
71
|
logger.warn('No tools enabled', 'CONFIG');
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const allTools = this.toolRegistry.getDefinitions();
|
|
85
|
-
const filteredTools = allTools.filter(tool => this.enabledTools.has(tool.name));
|
|
86
|
-
logger.config(`Available tools: ${filteredTools.map(t => t.name).join(', ')}`);
|
|
87
|
-
return { tools: filteredTools };
|
|
88
|
-
});
|
|
89
|
-
// Call tool
|
|
90
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
91
|
-
const { name: toolName, arguments: args } = request.params;
|
|
92
|
-
// Check if tool is enabled
|
|
93
|
-
if (!this.enabledTools.has(toolName)) {
|
|
94
|
-
throw new McpError(ErrorCode.InvalidRequest, `Tool '${toolName}' is not enabled. Enable it in your configuration.`);
|
|
95
|
-
}
|
|
96
|
-
// Get handler
|
|
97
|
-
const handler = this.toolRegistry.getHandler(toolName);
|
|
98
|
-
if (!handler) {
|
|
99
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
|
|
74
|
+
registerToolsAndResources(mcp) {
|
|
75
|
+
for (const def of this.toolRegistry.getDefinitions()) {
|
|
76
|
+
const inputSchema = toolDefinitionInputZod(def);
|
|
77
|
+
const registered = mcp.registerTool(def.name, {
|
|
78
|
+
...(def.title?.trim() ? { title: def.title.trim() } : {}),
|
|
79
|
+
description: def.description,
|
|
80
|
+
inputSchema,
|
|
81
|
+
...(def.annotations ? { annotations: def.annotations } : {}),
|
|
82
|
+
...(def.meta && Object.keys(def.meta).length > 0 ? { _meta: def.meta } : {}),
|
|
83
|
+
}, async (args) => this.invokeTool(def.name, args));
|
|
84
|
+
if (!this.enabledTools.has(def.name)) {
|
|
85
|
+
registered.disable();
|
|
100
86
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
87
|
+
}
|
|
88
|
+
for (const entry of STATIC_MCP_RESOURCE_ENTRIES) {
|
|
89
|
+
const uri = `${RESOURCE_URI_SCHEME}://${entry.slug}`;
|
|
90
|
+
mcp.registerResource(entry.name, uri, { description: entry.description, mimeType: entry.mimeType }, async (resourceUrl) => this.resourceHandler.readResource(resourceUrl.toString()));
|
|
91
|
+
}
|
|
92
|
+
logger.config(`Registered ${this.toolRegistry.getDefinitions().length} tool(s) (${this.enabledTools.size} enabled) and ${STATIC_MCP_RESOURCE_ENTRIES.length} resource(s) via McpServer`);
|
|
93
|
+
this.installStrictToolsCallHandling(mcp);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* MCP SDK maps most tool errors to CallToolResult with isError, but compliance harnesses expect
|
|
97
|
+
* JSON-RPC errors for unknown/disabled tool names.
|
|
98
|
+
*
|
|
99
|
+
* WARNING: accesses SDK internals (_requestHandlers, _registeredTools).
|
|
100
|
+
* Tested against @modelcontextprotocol/sdk ^1.28.0 — verify after upgrades.
|
|
101
|
+
*/
|
|
102
|
+
installStrictToolsCallHandling(mcp) {
|
|
103
|
+
const server = mcp.server;
|
|
104
|
+
const previous = server._requestHandlers?.get('tools/call');
|
|
105
|
+
if (!previous) {
|
|
106
|
+
logger.warn('Could not install strict tools/call handling — SDK internals may have changed', 'MCP');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const toolsMap = mcp
|
|
110
|
+
._registeredTools;
|
|
111
|
+
if (!toolsMap) {
|
|
112
|
+
logger.warn('Could not install strict tools/call handling — _registeredTools not found', 'MCP');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
mcp.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
116
|
+
const name = request.params.name;
|
|
117
|
+
const registered = toolsMap[name];
|
|
118
|
+
if (!registered) {
|
|
119
|
+
throw new McpError(ErrorCode.MethodNotFound, `Tool '${name}' not found`);
|
|
115
120
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
throw ErrorHandler.toMcpError(error, `${toolName} failed`);
|
|
121
|
+
if (!registered.enabled) {
|
|
122
|
+
throw new McpError(ErrorCode.InvalidParams, `Tool '${name}' is disabled`);
|
|
119
123
|
}
|
|
120
|
-
|
|
121
|
-
// Resources
|
|
122
|
-
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
123
|
-
return this.resourceHandler.listResources();
|
|
124
|
-
});
|
|
125
|
-
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
126
|
-
return this.resourceHandler.readResource(request.params.uri);
|
|
124
|
+
return previous(request, extra);
|
|
127
125
|
});
|
|
128
126
|
}
|
|
129
|
-
|
|
130
|
-
this.
|
|
127
|
+
async invokeTool(toolName, args) {
|
|
128
|
+
const handler = this.toolRegistry.getHandler(toolName);
|
|
129
|
+
if (!handler) {
|
|
130
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const subConfigs = this.toolSubConfigs.get(toolName);
|
|
134
|
+
const result = await handler(args, subConfigs);
|
|
135
|
+
if (!result.isError) {
|
|
136
|
+
const ackToken = this.toolAckTokens.get(toolName);
|
|
137
|
+
if (ackToken) {
|
|
138
|
+
result.content.unshift({
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: `Acknowledgment token: ${ackToken}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
ErrorHandler.logError(error, `tool: ${toolName}`);
|
|
148
|
+
throw ErrorHandler.toMcpError(error, `${toolName} failed`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
setupErrorHandling(mcp) {
|
|
152
|
+
mcp.server.onerror = (error) => {
|
|
131
153
|
logger.error('MCP protocol error', 'MCP', { error });
|
|
132
154
|
};
|
|
155
|
+
if (GrocyMcpServer.sigintHandlerRegistered) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
GrocyMcpServer.sigintHandlerRegistered = true;
|
|
133
159
|
process.on('SIGINT', async () => {
|
|
134
160
|
logger.info('Shutting down server...', 'SERVER');
|
|
135
|
-
await
|
|
161
|
+
await Promise.all(Array.from(GrocyMcpServer.activeServers).map((s) => s.close().catch(() => { })));
|
|
136
162
|
process.exit(0);
|
|
137
163
|
});
|
|
138
164
|
}
|
|
165
|
+
createMcpServer() {
|
|
166
|
+
const mcp = new McpServer(SERVER_INFO, {
|
|
167
|
+
capabilities: GROCY_SERVER_CAPABILITIES,
|
|
168
|
+
});
|
|
169
|
+
GrocyMcpServer.activeServers.add(mcp);
|
|
170
|
+
this.registerToolsAndResources(mcp);
|
|
171
|
+
this.setupErrorHandling(mcp);
|
|
172
|
+
return mcp;
|
|
173
|
+
}
|
|
139
174
|
async start() {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
const httpTransportOnly = process.env.MCP_HTTP_TRANSPORT_ONLY === 'true' || process.env.MCP_HTTP_TRANSPORT_ONLY === '1';
|
|
176
|
+
if (httpTransportOnly && !config.server.enable_http_server) {
|
|
177
|
+
logger.error('MCP_HTTP_TRANSPORT_ONLY is set but HTTP server is disabled; enable server.enable_http_server or ENABLE_HTTP_SERVER', 'SERVER');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
if (!httpTransportOnly) {
|
|
181
|
+
const transport = new StdioServerTransport();
|
|
182
|
+
await this.mcp.connect(transport);
|
|
183
|
+
logger.info('MCP server running on stdio', 'SERVER');
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
logger.info('MCP_HTTP_TRANSPORT_ONLY: stdio transport skipped', 'SERVER');
|
|
187
|
+
}
|
|
145
188
|
if (config.server.enable_http_server) {
|
|
146
189
|
try {
|
|
147
190
|
logger.config(`Starting HTTP server on port ${config.server.http_server_port}`);
|
|
148
|
-
const serverFactory = () => this.
|
|
149
|
-
await startHttpServer(serverFactory, config.server.http_server_port
|
|
191
|
+
const serverFactory = () => this.createMcpServer();
|
|
192
|
+
await startHttpServer(serverFactory, config.server.http_server_port, {
|
|
193
|
+
corsOrigin: config.server.http_cors_origin,
|
|
194
|
+
...(config.server.http_access_token !== undefined && {
|
|
195
|
+
accessToken: config.server.http_access_token,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
150
198
|
}
|
|
151
199
|
catch (error) {
|
|
152
200
|
logger.error('Failed to start HTTP server', 'SERVER', { error });
|
|
@@ -155,9 +203,8 @@ export class GrocyMcpServer {
|
|
|
155
203
|
}
|
|
156
204
|
}
|
|
157
205
|
}
|
|
158
|
-
// Expose server for HTTP transport
|
|
159
206
|
get serverInstance() {
|
|
160
|
-
return this.
|
|
207
|
+
return this.mcp;
|
|
161
208
|
}
|
|
162
209
|
}
|
|
163
210
|
export default GrocyMcpServer;
|
|
@@ -3,34 +3,42 @@ import { SERVER_NAME } from '../version.js';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
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
|
+
];
|
|
6
27
|
export class ResourceHandler {
|
|
7
28
|
__dirname;
|
|
29
|
+
allowedResources = new Set(STATIC_MCP_RESOURCE_ENTRIES.map((e) => e.slug));
|
|
8
30
|
constructor() {
|
|
9
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
32
|
this.__dirname = path.dirname(__filename);
|
|
11
33
|
}
|
|
12
34
|
async listResources() {
|
|
13
35
|
return {
|
|
14
|
-
resources:
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
uri: `${SERVER_NAME}://response-format`,
|
|
23
|
-
name: 'Response Format Documentation',
|
|
24
|
-
description: 'Documentation of the response format and structure',
|
|
25
|
-
mimeType: 'text/markdown'
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
uri: `${SERVER_NAME}://config`,
|
|
29
|
-
name: 'Configuration Documentation',
|
|
30
|
-
description: 'Documentation of all configuration options and how to use them',
|
|
31
|
-
mimeType: 'text/markdown'
|
|
32
|
-
}
|
|
33
|
-
]
|
|
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
|
+
})),
|
|
34
42
|
};
|
|
35
43
|
}
|
|
36
44
|
async readResource(uri) {
|
|
@@ -40,20 +48,25 @@ export class ResourceHandler {
|
|
|
40
48
|
throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI format: ${uri}`);
|
|
41
49
|
}
|
|
42
50
|
const resource = match[1];
|
|
51
|
+
if (!resource || !this.allowedResources.has(resource)) {
|
|
52
|
+
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${resource}`);
|
|
53
|
+
}
|
|
43
54
|
try {
|
|
44
55
|
// In the built app, resources are in build/resources
|
|
45
56
|
// In development, they're in src/resources
|
|
46
57
|
const resourcePath = path.join(this.__dirname, '../resources', `${resource}.md`);
|
|
47
58
|
const content = await fs.promises.readFile(resourcePath, 'utf8');
|
|
48
59
|
return {
|
|
49
|
-
contents: [
|
|
60
|
+
contents: [
|
|
61
|
+
{
|
|
50
62
|
uri,
|
|
51
63
|
mimeType: 'text/markdown',
|
|
52
|
-
text: content
|
|
53
|
-
}
|
|
64
|
+
text: content,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
54
67
|
};
|
|
55
68
|
}
|
|
56
|
-
catch
|
|
69
|
+
catch {
|
|
57
70
|
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${resource}`);
|
|
58
71
|
}
|
|
59
72
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
function propertyToZod(prop, required) {
|
|
10
|
+
const t = prop.type;
|
|
11
|
+
let inner;
|
|
12
|
+
switch (t) {
|
|
13
|
+
case 'string': {
|
|
14
|
+
if (Array.isArray(prop.enum) && isStringTupleEnum(prop.enum)) {
|
|
15
|
+
inner = z.enum(prop.enum);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
inner = z.string();
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case 'number':
|
|
23
|
+
case 'integer':
|
|
24
|
+
inner = z.number();
|
|
25
|
+
break;
|
|
26
|
+
case 'boolean':
|
|
27
|
+
inner = z.boolean();
|
|
28
|
+
break;
|
|
29
|
+
case 'array': {
|
|
30
|
+
const items = prop.items;
|
|
31
|
+
if (items?.type === 'string' && Array.isArray(items.enum) && isStringTupleEnum(items.enum)) {
|
|
32
|
+
inner = z.array(z.enum(items.enum));
|
|
33
|
+
}
|
|
34
|
+
else if (items?.type === 'string') {
|
|
35
|
+
inner = z.array(z.string());
|
|
36
|
+
}
|
|
37
|
+
else if (items?.type === 'number' || items?.type === 'integer') {
|
|
38
|
+
inner = z.array(z.number());
|
|
39
|
+
}
|
|
40
|
+
else if (items?.type === 'boolean') {
|
|
41
|
+
inner = z.array(z.boolean());
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
inner = z.array(z.unknown());
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'object': {
|
|
49
|
+
if (prop.properties && typeof prop.properties === 'object') {
|
|
50
|
+
inner = objectPropsToZod(prop.properties, prop.required);
|
|
51
|
+
}
|
|
52
|
+
else if (prop.additionalProperties &&
|
|
53
|
+
typeof prop.additionalProperties === 'object' &&
|
|
54
|
+
prop.additionalProperties.type === 'string') {
|
|
55
|
+
inner = z.record(z.string(), z.string());
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
inner = z.record(z.string(), z.unknown());
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
inner = z.unknown();
|
|
64
|
+
}
|
|
65
|
+
return required ? inner : inner.optional();
|
|
66
|
+
}
|
|
67
|
+
function objectPropsToZod(properties, requiredList) {
|
|
68
|
+
const required = new Set(requiredList ?? []);
|
|
69
|
+
const shape = {};
|
|
70
|
+
for (const [key, raw] of Object.entries(properties)) {
|
|
71
|
+
shape[key] = propertyToZod(raw, required.has(key));
|
|
72
|
+
}
|
|
73
|
+
return z.object(shape);
|
|
74
|
+
}
|
|
75
|
+
/** Converts a tool definition's JSON Schema object input into a Zod schema for MCP. */
|
|
76
|
+
export function toolDefinitionInputZod(def) {
|
|
77
|
+
const schema = def.inputSchema;
|
|
78
|
+
if (schema?.type !== 'object') {
|
|
79
|
+
return z.record(z.string(), z.unknown());
|
|
80
|
+
}
|
|
81
|
+
const props = schema.properties;
|
|
82
|
+
if (!props || typeof props !== 'object' || Object.keys(props).length === 0) {
|
|
83
|
+
return z.object({});
|
|
84
|
+
}
|
|
85
|
+
return objectPropsToZod(props, schema.required);
|
|
86
|
+
}
|
package/build/tools/base.js
CHANGED
|
@@ -13,13 +13,13 @@ export class BaseToolHandler {
|
|
|
13
13
|
content: [
|
|
14
14
|
{
|
|
15
15
|
type: 'text',
|
|
16
|
-
text: message || 'Operation completed successfully'
|
|
16
|
+
text: message || 'Operation completed successfully',
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
type: 'text',
|
|
20
|
-
text: JSON.stringify(data, null, 2)
|
|
21
|
-
}
|
|
22
|
-
]
|
|
20
|
+
text: JSON.stringify(data, null, 2),
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
@@ -30,21 +30,25 @@ export class BaseToolHandler {
|
|
|
30
30
|
content: [
|
|
31
31
|
{
|
|
32
32
|
type: 'text',
|
|
33
|
-
text: `Error: ${message}
|
|
33
|
+
text: `Error: ${message}`,
|
|
34
34
|
},
|
|
35
|
-
...(details
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
...(details
|
|
36
|
+
? [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: JSON.stringify(details, null, 2),
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
: []),
|
|
39
43
|
],
|
|
40
|
-
isError: true
|
|
44
|
+
isError: true,
|
|
41
45
|
};
|
|
42
46
|
}
|
|
43
47
|
/**
|
|
44
48
|
* Validate required parameters
|
|
45
49
|
*/
|
|
46
50
|
validateRequired(params, required) {
|
|
47
|
-
const missing = required.filter(field => {
|
|
51
|
+
const missing = required.filter((field) => {
|
|
48
52
|
const value = params[field];
|
|
49
53
|
return value === undefined || value === null || value === '';
|
|
50
54
|
});
|
|
@@ -60,7 +64,7 @@ export class BaseToolHandler {
|
|
|
60
64
|
const response = await apiClient.request(endpoint, {
|
|
61
65
|
method,
|
|
62
66
|
body: data,
|
|
63
|
-
queryParams: options?.queryParams || {}
|
|
67
|
+
queryParams: options?.queryParams || {},
|
|
64
68
|
});
|
|
65
69
|
return response.data;
|
|
66
70
|
}, `API ${method} ${endpoint}`);
|
|
@@ -97,9 +101,9 @@ export class BaseToolHandler {
|
|
|
97
101
|
* Filter object fields based on allowlist
|
|
98
102
|
*/
|
|
99
103
|
filterFields(objects, fields) {
|
|
100
|
-
return objects.map(obj => {
|
|
104
|
+
return objects.map((obj) => {
|
|
101
105
|
const filtered = {};
|
|
102
|
-
fields.forEach(field => {
|
|
106
|
+
fields.forEach((field) => {
|
|
103
107
|
if (field in obj) {
|
|
104
108
|
filtered[field] = obj[field];
|
|
105
109
|
}
|
|
@@ -120,7 +124,7 @@ export class BaseToolHandler {
|
|
|
120
124
|
if (value.length === 0) {
|
|
121
125
|
throw new ValidationError(`${paramName} cannot be empty`);
|
|
122
126
|
}
|
|
123
|
-
return value.map(v => String(v));
|
|
127
|
+
return value.map((v) => String(v));
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
126
130
|
* Parse and validate numeric parameter
|