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.
- package/CHANGELOG.md +45 -0
- package/README.md +125 -32
- package/build/resources/CHANGELOG.md +47 -0
- package/build/resources/DOCS.md +8 -3
- package/build/resources/README.md +125 -32
- package/build/resources/api-reference.md +88 -88
- package/build/resources/config.md +45 -17
- package/build/resources/examples.md +120 -221
- package/build/resources/installation.md +9 -14
- package/build/resources/response-format.md +29 -20
- package/build/version.js +2 -2
- package/package.json +50 -24
- package/build/api/client.js +0 -121
- package/build/config/index.js +0 -205
- package/build/main.js +0 -44
- package/build/server/http-server.js +0 -218
- package/build/server/mcp-server.js +0 -163
- package/build/server/resources.js +0 -60
- package/build/tools/base.js +0 -141
- package/build/tools/household/definitions.js +0 -157
- package/build/tools/household/handlers.js +0 -109
- package/build/tools/household/index.js +0 -25
- package/build/tools/index.js +0 -2
- package/build/tools/inventory/definitions.js +0 -379
- package/build/tools/inventory/handlers.js +0 -430
- package/build/tools/inventory/index.js +0 -32
- package/build/tools/module-loader.js +0 -151
- package/build/tools/recipes/definitions.js +0 -261
- package/build/tools/recipes/handlers.js +0 -471
- package/build/tools/recipes/index.js +0 -33
- package/build/tools/recipes/validations.js +0 -23
- package/build/tools/shopping/definitions.js +0 -71
- package/build/tools/shopping/handlers.js +0 -43
- package/build/tools/shopping/index.js +0 -14
- package/build/tools/system/definitions.js +0 -86
- package/build/tools/system/handlers.js +0 -94
- package/build/tools/system/index.js +0 -16
- package/build/tools/types.js +0 -1
- package/build/tools/validation-helpers.js +0 -36
- package/build/types/index.js +0 -62
- package/build/utils/errors.js +0 -138
- package/build/utils/logger.js +0 -141
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
-
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
5
|
-
import { VERSION, PACKAGE_NAME as SERVER_NAME } from '../version.js';
|
|
6
|
-
import cors from 'cors';
|
|
7
|
-
import http from 'http';
|
|
8
|
-
import { logger } from '../utils/logger.js';
|
|
9
|
-
// HTTP Transport for MCP (Context7 style)
|
|
10
|
-
export function startHttpServer(mcpServer, port = 8080) {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const app = express();
|
|
13
|
-
// Enable JSON body parsing with increased limit
|
|
14
|
-
app.use(express.json({
|
|
15
|
-
limit: '10mb'
|
|
16
|
-
}));
|
|
17
|
-
// Enable CORS for all routes
|
|
18
|
-
app.use(cors({
|
|
19
|
-
origin: '*',
|
|
20
|
-
methods: ['GET', 'POST', 'OPTIONS'],
|
|
21
|
-
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Mcp-Session-Id', 'Authorization'],
|
|
22
|
-
exposedHeaders: ['Mcp-Session-Id', 'Content-Type'],
|
|
23
|
-
optionsSuccessStatus: 200
|
|
24
|
-
}));
|
|
25
|
-
// Simple health check endpoint
|
|
26
|
-
app.get('/', (_req, res) => {
|
|
27
|
-
res.json({
|
|
28
|
-
status: 'ok',
|
|
29
|
-
service: SERVER_NAME,
|
|
30
|
-
version: VERSION,
|
|
31
|
-
message: 'MCP server is running',
|
|
32
|
-
endpoints: {
|
|
33
|
-
streamable: '/mcp',
|
|
34
|
-
sse: '/mcp/sse',
|
|
35
|
-
sseMessages: '/mcp/messages'
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
// Session management for transports
|
|
40
|
-
const streamableTransports = {};
|
|
41
|
-
const sseTransports = {};
|
|
42
|
-
const sseServerInstances = {};
|
|
43
|
-
// Simplified request logging
|
|
44
|
-
app.use((req, _res, next) => {
|
|
45
|
-
logger.server(`${req.method} ${req.path}`);
|
|
46
|
-
next();
|
|
47
|
-
});
|
|
48
|
-
// Helper function to get server instance (shared or new)
|
|
49
|
-
const getServerInstance = () => {
|
|
50
|
-
if (typeof mcpServer === 'function') {
|
|
51
|
-
return mcpServer(); // Create new instance
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
return mcpServer; // Use shared instance
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
// Streamable HTTP endpoint (Context7 modern)
|
|
58
|
-
app.post('/mcp', async (req, res) => {
|
|
59
|
-
try {
|
|
60
|
-
const clientSessionId = req.headers['mcp-session-id'];
|
|
61
|
-
let transport = undefined;
|
|
62
|
-
// Accept header check (can be done early)
|
|
63
|
-
const accept = req.headers.accept || '';
|
|
64
|
-
if (!accept.includes('application/json') && !accept.includes('text/event-stream')) {
|
|
65
|
-
logger.error('Client must accept application/json or text/event-stream', 'server');
|
|
66
|
-
res.status(406).json({
|
|
67
|
-
jsonrpc: '2.0',
|
|
68
|
-
error: {
|
|
69
|
-
code: -32000,
|
|
70
|
-
message: 'Not Acceptable: Client must accept application/json or text/event-stream'
|
|
71
|
-
},
|
|
72
|
-
id: req.body?.id || null
|
|
73
|
-
});
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
if (clientSessionId) {
|
|
77
|
-
transport = streamableTransports[clientSessionId];
|
|
78
|
-
if (!transport) {
|
|
79
|
-
res.status(400).json({
|
|
80
|
-
jsonrpc: '2.0',
|
|
81
|
-
error: { code: -32001, message: `Invalid or expired session ID: ${clientSessionId}. Please re-initialize.` },
|
|
82
|
-
id: req.body?.id || null
|
|
83
|
-
});
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
// No session ID provided by client, create new transport
|
|
89
|
-
const newGeneratedSessionId = randomUUID();
|
|
90
|
-
const newTransportInstance = new StreamableHTTPServerTransport({
|
|
91
|
-
sessionIdGenerator: () => newGeneratedSessionId
|
|
92
|
-
});
|
|
93
|
-
transport = newTransportInstance;
|
|
94
|
-
streamableTransports[newGeneratedSessionId] = transport;
|
|
95
|
-
transport.onclose = () => {
|
|
96
|
-
const closedSessionId = transport?.sessionId || newGeneratedSessionId;
|
|
97
|
-
delete streamableTransports[closedSessionId];
|
|
98
|
-
};
|
|
99
|
-
const serverInstance = getServerInstance();
|
|
100
|
-
await serverInstance.connect(transport); // Type assertion to work around SDK type issue
|
|
101
|
-
}
|
|
102
|
-
if (!transport) {
|
|
103
|
-
logger.error('Transport is undefined before handling request. This should not happen.', 'server');
|
|
104
|
-
res.status(500).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Internal server error: Transport not available' }, id: req.body?.id || null });
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (transport.sessionId) {
|
|
108
|
-
res.setHeader('Mcp-Session-Id', transport.sessionId);
|
|
109
|
-
}
|
|
110
|
-
await transport.handleRequest(req, res, req.body);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
logger.error('Failed to handle streamable HTTP request', 'server', { error });
|
|
114
|
-
// Send error response if headers not sent yet
|
|
115
|
-
if (!res.headersSent) {
|
|
116
|
-
res.status(500).json({
|
|
117
|
-
jsonrpc: '2.0',
|
|
118
|
-
error: {
|
|
119
|
-
code: -32000,
|
|
120
|
-
message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`
|
|
121
|
-
},
|
|
122
|
-
id: req.body?.id || null
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
// SSE endpoint
|
|
128
|
-
app.get('/mcp/sse', async (_req, res) => {
|
|
129
|
-
try {
|
|
130
|
-
// Set SSE headers before creating transport
|
|
131
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
132
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
133
|
-
res.setHeader('Connection', 'keep-alive');
|
|
134
|
-
const transport = new SSEServerTransport('/mcp/messages', res);
|
|
135
|
-
const sessionId = transport.sessionId;
|
|
136
|
-
sseTransports[sessionId] = transport;
|
|
137
|
-
// Handle connection cleanup
|
|
138
|
-
const cleanup = () => {
|
|
139
|
-
delete sseTransports[sessionId];
|
|
140
|
-
delete sseServerInstances[sessionId];
|
|
141
|
-
};
|
|
142
|
-
res.on('close', cleanup);
|
|
143
|
-
res.on('error', (err) => {
|
|
144
|
-
logger.error(`SSE connection error for session ${sessionId}`, 'server', { error: err });
|
|
145
|
-
cleanup();
|
|
146
|
-
});
|
|
147
|
-
// Create isolated server instance for this SSE connection to prevent response cross-talk
|
|
148
|
-
const isolatedServer = getServerInstance();
|
|
149
|
-
sseServerInstances[sessionId] = isolatedServer;
|
|
150
|
-
// Connect transport to isolated MCP server (non-blocking)
|
|
151
|
-
isolatedServer.connect(transport).catch((error) => {
|
|
152
|
-
logger.error(`Failed to connect SSE transport for session ${sessionId}`, 'server', { error });
|
|
153
|
-
cleanup();
|
|
154
|
-
if (!res.headersSent) {
|
|
155
|
-
res.status(500).end();
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
// Send initial comment to keep connection alive
|
|
159
|
-
res.write(': connected\n\n');
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
logger.error('Failed to handle SSE connection', 'server', { error });
|
|
163
|
-
if (!res.headersSent) {
|
|
164
|
-
res.status(500).send('Internal Server Error');
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
res.end();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
// Message endpoint for SSE
|
|
172
|
-
app.post('/mcp/messages', async (req, res) => {
|
|
173
|
-
const sessionId = req.query.sessionId;
|
|
174
|
-
if (!sessionId) {
|
|
175
|
-
res.status(400).json({
|
|
176
|
-
error: 'Missing sessionId parameter',
|
|
177
|
-
status: 400
|
|
178
|
-
});
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const transport = sseTransports[sessionId];
|
|
182
|
-
if (transport) {
|
|
183
|
-
try {
|
|
184
|
-
await transport.handlePostMessage(req, res, req.body);
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
logger.error(`Failed to handle SSE message for session ${sessionId}`, 'server', { error });
|
|
188
|
-
res.status(500).json({
|
|
189
|
-
error: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,
|
|
190
|
-
status: 500
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
res.status(404).json({
|
|
196
|
-
error: `No active SSE connection found for session ID: ${sessionId}`,
|
|
197
|
-
status: 404
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
// Create HTTP server with explicit error handling
|
|
202
|
-
const server = http.createServer(app);
|
|
203
|
-
server.on('error', (error) => {
|
|
204
|
-
logger.error(`HTTP server error: ${error.message}`, 'server');
|
|
205
|
-
reject(error);
|
|
206
|
-
});
|
|
207
|
-
// Start the server
|
|
208
|
-
server.listen(port, () => {
|
|
209
|
-
logger.server(`HTTP server listening on port ${port}`);
|
|
210
|
-
logger.server(`Available endpoints:`);
|
|
211
|
-
logger.server(` - Health check: http://localhost:${port}/`);
|
|
212
|
-
logger.server(` - Streamable HTTP: http://localhost:${port}/mcp`);
|
|
213
|
-
logger.server(` - SSE: http://localhost:${port}/mcp/sse`);
|
|
214
|
-
logger.server(` - SSE Messages: http://localhost:${port}/mcp/messages`);
|
|
215
|
-
resolve(server);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simplified MCP Server implementation
|
|
3
|
-
* Reduced complexity and improved performance
|
|
4
|
-
*/
|
|
5
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
-
import { z } from 'zod';
|
|
9
|
-
import { VERSION, PACKAGE_NAME as SERVER_NAME } from '../version.js';
|
|
10
|
-
import { createToolRegistry } from '../tools/index.js';
|
|
11
|
-
import { config } from '../config/index.js';
|
|
12
|
-
import { startHttpServer } from './http-server.js';
|
|
13
|
-
import { ResourceHandler } from './resources.js';
|
|
14
|
-
import { logger } from '../utils/logger.js';
|
|
15
|
-
import { ErrorHandler } from '../utils/errors.js';
|
|
16
|
-
export class GrocyMcpServer {
|
|
17
|
-
server;
|
|
18
|
-
enabledTools = new Set();
|
|
19
|
-
toolSubConfigs = new Map();
|
|
20
|
-
toolAckTokens = new Map();
|
|
21
|
-
resourceHandler;
|
|
22
|
-
toolRegistry;
|
|
23
|
-
constructor(server, toolRegistry, resourceHandler) {
|
|
24
|
-
this.server = server;
|
|
25
|
-
this.toolRegistry = toolRegistry;
|
|
26
|
-
this.resourceHandler = resourceHandler;
|
|
27
|
-
this.parseToolConfiguration();
|
|
28
|
-
this.setupHandlers();
|
|
29
|
-
this.setupErrorHandling();
|
|
30
|
-
}
|
|
31
|
-
static async create() {
|
|
32
|
-
// Initialize components
|
|
33
|
-
const [toolRegistry, resourceHandler] = await Promise.all([
|
|
34
|
-
createToolRegistry(),
|
|
35
|
-
Promise.resolve(new ResourceHandler())
|
|
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
|
-
}
|
|
49
|
-
});
|
|
50
|
-
return new GrocyMcpServer(server, toolRegistry, resourceHandler);
|
|
51
|
-
}
|
|
52
|
-
parseToolConfiguration() {
|
|
53
|
-
const { enabledTools, toolSubConfigs, toolAckTokens } = config.parseToolConfiguration();
|
|
54
|
-
this.toolSubConfigs = toolSubConfigs;
|
|
55
|
-
this.toolAckTokens = toolAckTokens;
|
|
56
|
-
// Validate tool names
|
|
57
|
-
const validToolNames = new Set(this.toolRegistry.getToolNames());
|
|
58
|
-
if (enabledTools.size > 0) {
|
|
59
|
-
const invalidTools = Array.from(enabledTools).filter(tool => !validToolNames.has(tool));
|
|
60
|
-
if (invalidTools.length > 0) {
|
|
61
|
-
const validNames = Array.from(validToolNames).sort().join(', ');
|
|
62
|
-
logger.error(`Invalid tools: ${invalidTools.join(', ')}. Valid: ${validNames}`, 'CONFIG');
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
this.enabledTools = enabledTools;
|
|
66
|
-
logger.config(`Enabled tools: ${Array.from(enabledTools).join(', ')}`);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
logger.warn('No tools enabled', 'CONFIG');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
setupHandlers() {
|
|
73
|
-
// Initialize handler
|
|
74
|
-
this.server.setRequestHandler(z.object({ method: z.literal('initialize'), params: z.any().optional() }), async (request) => {
|
|
75
|
-
logger.debug('Initialize request', 'MCP');
|
|
76
|
-
return {
|
|
77
|
-
protocolVersion: request.params?.protocolVersion || '2024-11-05',
|
|
78
|
-
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
79
|
-
serverInfo: { name: SERVER_NAME, version: VERSION }
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
// List tools
|
|
83
|
-
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
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}`);
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
const subConfigs = this.toolSubConfigs.get(toolName);
|
|
103
|
-
const result = await handler(args, subConfigs);
|
|
104
|
-
// Automatically add ack_token to successful responses (at the beginning)
|
|
105
|
-
if (!result.isError) {
|
|
106
|
-
const ackToken = this.toolAckTokens.get(toolName);
|
|
107
|
-
if (ackToken) {
|
|
108
|
-
result.content.unshift({
|
|
109
|
-
type: 'text',
|
|
110
|
-
text: `Acknowledgment token: ${ackToken}`
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
ErrorHandler.logError(error, `tool: ${toolName}`);
|
|
118
|
-
throw ErrorHandler.toMcpError(error, `${toolName} failed`);
|
|
119
|
-
}
|
|
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);
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
setupErrorHandling() {
|
|
130
|
-
this.server.onerror = (error) => {
|
|
131
|
-
logger.error('MCP protocol error', 'MCP', { error });
|
|
132
|
-
};
|
|
133
|
-
process.on('SIGINT', async () => {
|
|
134
|
-
logger.info('Shutting down server...', 'SERVER');
|
|
135
|
-
await this.server.close();
|
|
136
|
-
process.exit(0);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
async start() {
|
|
140
|
-
// Start STDIO transport
|
|
141
|
-
const transport = new StdioServerTransport();
|
|
142
|
-
await this.server.connect(transport);
|
|
143
|
-
logger.info('MCP server running on stdio', 'SERVER');
|
|
144
|
-
// Start HTTP/SSE if enabled
|
|
145
|
-
if (config.server.enable_http_server) {
|
|
146
|
-
try {
|
|
147
|
-
logger.config(`Starting HTTP server on port ${config.server.http_server_port}`);
|
|
148
|
-
const serverFactory = () => this.server;
|
|
149
|
-
await startHttpServer(serverFactory, config.server.http_server_port);
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
logger.error('Failed to start HTTP server', 'SERVER', { error });
|
|
153
|
-
logger.error('HTTP server is explicitly enabled but cannot start - exiting', 'SERVER');
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// Expose server for HTTP transport
|
|
159
|
-
get serverInstance() {
|
|
160
|
-
return this.server;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
export default GrocyMcpServer;
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
export class ResourceHandler {
|
|
7
|
-
__dirname;
|
|
8
|
-
constructor() {
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
this.__dirname = path.dirname(__filename);
|
|
11
|
-
}
|
|
12
|
-
async listResources() {
|
|
13
|
-
return {
|
|
14
|
-
resources: [
|
|
15
|
-
{
|
|
16
|
-
uri: `${SERVER_NAME}://examples`,
|
|
17
|
-
name: 'Grocy API Usage Examples',
|
|
18
|
-
description: 'Detailed examples of using the Grocy API',
|
|
19
|
-
mimeType: 'text/markdown'
|
|
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
|
-
]
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
async readResource(uri) {
|
|
37
|
-
const uriPattern = new RegExp(`^${SERVER_NAME}://(.+)$`);
|
|
38
|
-
const match = uri.match(uriPattern);
|
|
39
|
-
if (!match) {
|
|
40
|
-
throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI format: ${uri}`);
|
|
41
|
-
}
|
|
42
|
-
const resource = match[1];
|
|
43
|
-
try {
|
|
44
|
-
// In the built app, resources are in build/resources
|
|
45
|
-
// In development, they're in src/resources
|
|
46
|
-
const resourcePath = path.join(this.__dirname, '../resources', `${resource}.md`);
|
|
47
|
-
const content = await fs.promises.readFile(resourcePath, 'utf8');
|
|
48
|
-
return {
|
|
49
|
-
contents: [{
|
|
50
|
-
uri,
|
|
51
|
-
mimeType: 'text/markdown',
|
|
52
|
-
text: content
|
|
53
|
-
}]
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${resource}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
package/build/tools/base.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simplified base tool handler
|
|
3
|
-
*/
|
|
4
|
-
import apiClient from '../api/client.js';
|
|
5
|
-
import { ErrorHandler, ValidationError } from '../utils/errors.js';
|
|
6
|
-
import { logger } from '../utils/logger.js';
|
|
7
|
-
export class BaseToolHandler {
|
|
8
|
-
/**
|
|
9
|
-
* Create a standardized success result
|
|
10
|
-
*/
|
|
11
|
-
createSuccess(data, message) {
|
|
12
|
-
return {
|
|
13
|
-
content: [
|
|
14
|
-
{
|
|
15
|
-
type: 'text',
|
|
16
|
-
text: message || 'Operation completed successfully'
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
type: 'text',
|
|
20
|
-
text: JSON.stringify(data, null, 2)
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Create a standardized error result
|
|
27
|
-
*/
|
|
28
|
-
createError(message, details) {
|
|
29
|
-
return {
|
|
30
|
-
content: [
|
|
31
|
-
{
|
|
32
|
-
type: 'text',
|
|
33
|
-
text: `Error: ${message}`
|
|
34
|
-
},
|
|
35
|
-
...(details ? [{
|
|
36
|
-
type: 'text',
|
|
37
|
-
text: JSON.stringify(details, null, 2)
|
|
38
|
-
}] : [])
|
|
39
|
-
],
|
|
40
|
-
isError: true
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Validate required parameters
|
|
45
|
-
*/
|
|
46
|
-
validateRequired(params, required) {
|
|
47
|
-
const missing = required.filter(field => {
|
|
48
|
-
const value = params[field];
|
|
49
|
-
return value === undefined || value === null || value === '';
|
|
50
|
-
});
|
|
51
|
-
if (missing.length > 0) {
|
|
52
|
-
throw new ValidationError(`Missing required parameters: ${missing.join(', ')}`, 'parameter validation');
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Safely make API calls with error handling
|
|
57
|
-
*/
|
|
58
|
-
async apiCall(endpoint, method = 'GET', data, options) {
|
|
59
|
-
return ErrorHandler.handleAsync(async () => {
|
|
60
|
-
const response = await apiClient.request(endpoint, {
|
|
61
|
-
method,
|
|
62
|
-
body: data,
|
|
63
|
-
queryParams: options?.queryParams || {}
|
|
64
|
-
});
|
|
65
|
-
return response.data;
|
|
66
|
-
}, `API ${method} ${endpoint}`);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Handle tool execution with standardized error handling
|
|
70
|
-
*/
|
|
71
|
-
async executeToolHandler(handler) {
|
|
72
|
-
try {
|
|
73
|
-
return await handler();
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
ErrorHandler.logError(error, 'tool execution');
|
|
77
|
-
if (error instanceof ValidationError) {
|
|
78
|
-
return this.createError(error.message);
|
|
79
|
-
}
|
|
80
|
-
const message = error instanceof Error ? error.message : 'Internal error';
|
|
81
|
-
return this.createError(`Tool execution failed: ${message}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Safe JSON formatting
|
|
86
|
-
*/
|
|
87
|
-
safeStringify(data) {
|
|
88
|
-
try {
|
|
89
|
-
return JSON.stringify(data, null, 2);
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
logger.warn('Failed to stringify data', 'TOOLS', { error });
|
|
93
|
-
return '[Unable to format data]';
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Filter object fields based on allowlist
|
|
98
|
-
*/
|
|
99
|
-
filterFields(objects, fields) {
|
|
100
|
-
return objects.map(obj => {
|
|
101
|
-
const filtered = {};
|
|
102
|
-
fields.forEach(field => {
|
|
103
|
-
if (field in obj) {
|
|
104
|
-
filtered[field] = obj[field];
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
return filtered;
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Parse and validate array parameter
|
|
112
|
-
*/
|
|
113
|
-
parseArrayParam(value, paramName) {
|
|
114
|
-
if (!value) {
|
|
115
|
-
throw new ValidationError(`${paramName} is required`);
|
|
116
|
-
}
|
|
117
|
-
if (!Array.isArray(value)) {
|
|
118
|
-
throw new ValidationError(`${paramName} must be an array`);
|
|
119
|
-
}
|
|
120
|
-
if (value.length === 0) {
|
|
121
|
-
throw new ValidationError(`${paramName} cannot be empty`);
|
|
122
|
-
}
|
|
123
|
-
return value.map(v => String(v));
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Parse and validate numeric parameter
|
|
127
|
-
*/
|
|
128
|
-
parseNumberParam(value, paramName, required = true) {
|
|
129
|
-
if (value === undefined || value === null) {
|
|
130
|
-
if (required) {
|
|
131
|
-
throw new ValidationError(`${paramName} is required`);
|
|
132
|
-
}
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
const num = Number(value);
|
|
136
|
-
if (isNaN(num)) {
|
|
137
|
-
throw new ValidationError(`${paramName} must be a valid number`);
|
|
138
|
-
}
|
|
139
|
-
return num;
|
|
140
|
-
}
|
|
141
|
-
}
|