mcp-grocy 1.9.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/LICENSE +23 -0
- package/README.md +285 -0
- package/build/api/client.js +154 -0
- package/build/config/environment.js +142 -0
- package/build/main.js +29 -0
- package/build/resources/CHANGELOG.md +352 -0
- package/build/resources/DOCS.md +62 -0
- package/build/resources/README.md +285 -0
- package/build/resources/api-reference.md +92 -0
- package/build/resources/config.md +282 -0
- package/build/resources/examples.md +319 -0
- package/build/resources/installation.md +117 -0
- package/build/resources/response-format.md +165 -0
- package/build/server/http-server.js +214 -0
- package/build/server/mcp-server.js +173 -0
- package/build/server/resources.js +60 -0
- package/build/tools/base.js +56 -0
- package/build/tools/index.js +494 -0
- package/build/tools/products/definitions.js +57 -0
- package/build/tools/products/handlers.js +78 -0
- package/build/tools/products/index.js +13 -0
- package/build/tools/recipes/definitions.js +191 -0
- package/build/tools/recipes/handlers.js +258 -0
- package/build/tools/recipes/index.js +18 -0
- package/build/tools/shopping/index.js +107 -0
- package/build/tools/stock/definitions.js +234 -0
- package/build/tools/stock/handlers.js +391 -0
- package/build/tools/stock/index.js +19 -0
- package/build/tools/system/index.js +230 -0
- package/build/tools/types.js +1 -0
- package/build/version.js +4 -0
- package/package.json +87 -0
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
// HTTP Transport for MCP (Context7 style)
|
|
9
|
+
export function startHttpServer(mcpServer, port = 8080) {
|
|
10
|
+
const app = express();
|
|
11
|
+
// Enable JSON body parsing with increased limit
|
|
12
|
+
app.use(express.json({
|
|
13
|
+
limit: '10mb'
|
|
14
|
+
}));
|
|
15
|
+
// Enable CORS for all routes
|
|
16
|
+
app.use(cors({
|
|
17
|
+
origin: '*',
|
|
18
|
+
methods: ['GET', 'POST', 'OPTIONS'],
|
|
19
|
+
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Mcp-Session-Id', 'Authorization'],
|
|
20
|
+
exposedHeaders: ['Mcp-Session-Id', 'Content-Type'],
|
|
21
|
+
optionsSuccessStatus: 200
|
|
22
|
+
}));
|
|
23
|
+
// Simple health check endpoint
|
|
24
|
+
app.get('/', (req, res) => {
|
|
25
|
+
res.json({
|
|
26
|
+
status: 'ok',
|
|
27
|
+
service: SERVER_NAME,
|
|
28
|
+
version: VERSION,
|
|
29
|
+
message: 'MCP server is running',
|
|
30
|
+
endpoints: {
|
|
31
|
+
streamable: '/mcp',
|
|
32
|
+
sse: '/mcp/sse',
|
|
33
|
+
sseMessages: '/mcp/messages'
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
// Session management for transports
|
|
38
|
+
const streamableTransports = {};
|
|
39
|
+
const sseTransports = {};
|
|
40
|
+
const sseServerInstances = {};
|
|
41
|
+
// Simplified request logging
|
|
42
|
+
app.use((req, res, next) => {
|
|
43
|
+
console.error(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
|
|
44
|
+
next();
|
|
45
|
+
});
|
|
46
|
+
// Helper function to get server instance (shared or new)
|
|
47
|
+
const getServerInstance = () => {
|
|
48
|
+
if (typeof mcpServer === 'function') {
|
|
49
|
+
return mcpServer(); // Create new instance
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
return mcpServer; // Use shared instance
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
// Streamable HTTP endpoint (Context7 modern)
|
|
56
|
+
app.post('/mcp', async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const clientSessionId = req.headers['mcp-session-id'];
|
|
59
|
+
let transport = undefined;
|
|
60
|
+
// Accept header check (can be done early)
|
|
61
|
+
const accept = req.headers.accept || '';
|
|
62
|
+
if (!accept.includes('application/json') && !accept.includes('text/event-stream')) {
|
|
63
|
+
console.error('[ERROR] Client must accept application/json or text/event-stream');
|
|
64
|
+
res.status(406).json({
|
|
65
|
+
jsonrpc: '2.0',
|
|
66
|
+
error: {
|
|
67
|
+
code: -32000,
|
|
68
|
+
message: 'Not Acceptable: Client must accept application/json or text/event-stream'
|
|
69
|
+
},
|
|
70
|
+
id: req.body?.id || null
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (clientSessionId) {
|
|
75
|
+
transport = streamableTransports[clientSessionId];
|
|
76
|
+
if (!transport) {
|
|
77
|
+
res.status(400).json({
|
|
78
|
+
jsonrpc: '2.0',
|
|
79
|
+
error: { code: -32001, message: `Invalid or expired session ID: ${clientSessionId}. Please re-initialize.` },
|
|
80
|
+
id: req.body?.id || null
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// No session ID provided by client, create new transport
|
|
87
|
+
const newGeneratedSessionId = randomUUID();
|
|
88
|
+
const newTransportInstance = new StreamableHTTPServerTransport({
|
|
89
|
+
sessionIdGenerator: () => newGeneratedSessionId
|
|
90
|
+
});
|
|
91
|
+
transport = newTransportInstance;
|
|
92
|
+
streamableTransports[newGeneratedSessionId] = transport;
|
|
93
|
+
transport.onclose = () => {
|
|
94
|
+
const closedSessionId = transport?.sessionId || newGeneratedSessionId;
|
|
95
|
+
delete streamableTransports[closedSessionId];
|
|
96
|
+
};
|
|
97
|
+
const serverInstance = getServerInstance();
|
|
98
|
+
await serverInstance.connect(transport);
|
|
99
|
+
}
|
|
100
|
+
if (!transport) {
|
|
101
|
+
console.error('[CRITICAL_ERROR] Transport is undefined before handling request. This should not happen.');
|
|
102
|
+
res.status(500).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Internal server error: Transport not available' }, id: req.body?.id || null });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (transport.sessionId) {
|
|
106
|
+
res.setHeader('Mcp-Session-Id', transport.sessionId);
|
|
107
|
+
}
|
|
108
|
+
await transport.handleRequest(req, res, req.body);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error('[ERROR] Failed to handle streamable HTTP request:', error);
|
|
112
|
+
// Send error response if headers not sent yet
|
|
113
|
+
if (!res.headersSent) {
|
|
114
|
+
res.status(500).json({
|
|
115
|
+
jsonrpc: '2.0',
|
|
116
|
+
error: {
|
|
117
|
+
code: -32000,
|
|
118
|
+
message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`
|
|
119
|
+
},
|
|
120
|
+
id: req.body?.id || null
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// SSE endpoint
|
|
126
|
+
app.get('/mcp/sse', async (_req, res) => {
|
|
127
|
+
try {
|
|
128
|
+
// Set SSE headers before creating transport
|
|
129
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
130
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
131
|
+
res.setHeader('Connection', 'keep-alive');
|
|
132
|
+
const transport = new SSEServerTransport('/mcp/messages', res);
|
|
133
|
+
const sessionId = transport.sessionId;
|
|
134
|
+
sseTransports[sessionId] = transport;
|
|
135
|
+
// Handle connection cleanup
|
|
136
|
+
const cleanup = () => {
|
|
137
|
+
delete sseTransports[sessionId];
|
|
138
|
+
delete sseServerInstances[sessionId];
|
|
139
|
+
};
|
|
140
|
+
res.on('close', cleanup);
|
|
141
|
+
res.on('error', (err) => {
|
|
142
|
+
console.error(`[ERROR] SSE connection error for session ${sessionId}:`, err);
|
|
143
|
+
cleanup();
|
|
144
|
+
});
|
|
145
|
+
// Create isolated server instance for this SSE connection to prevent response cross-talk
|
|
146
|
+
const isolatedServer = getServerInstance();
|
|
147
|
+
sseServerInstances[sessionId] = isolatedServer;
|
|
148
|
+
// Connect transport to isolated MCP server (non-blocking)
|
|
149
|
+
isolatedServer.connect(transport).catch((error) => {
|
|
150
|
+
console.error(`[ERROR] Failed to connect SSE transport for session ${sessionId}:`, error);
|
|
151
|
+
cleanup();
|
|
152
|
+
if (!res.headersSent) {
|
|
153
|
+
res.status(500).end();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// Send initial comment to keep connection alive
|
|
157
|
+
res.write(': connected\n\n');
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error('[ERROR] Failed to handle SSE connection:', error);
|
|
161
|
+
if (!res.headersSent) {
|
|
162
|
+
res.status(500).send('Internal Server Error');
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
res.end();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Message endpoint for SSE
|
|
170
|
+
app.post('/mcp/messages', async (req, res) => {
|
|
171
|
+
const sessionId = req.query.sessionId;
|
|
172
|
+
if (!sessionId) {
|
|
173
|
+
res.status(400).json({
|
|
174
|
+
error: 'Missing sessionId parameter',
|
|
175
|
+
status: 400
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const transport = sseTransports[sessionId];
|
|
180
|
+
if (transport) {
|
|
181
|
+
try {
|
|
182
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error(`[ERROR] Failed to handle SSE message for session ${sessionId}:`, error);
|
|
186
|
+
res.status(500).json({
|
|
187
|
+
error: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,
|
|
188
|
+
status: 500
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
res.status(404).json({
|
|
194
|
+
error: `No active SSE connection found for session ID: ${sessionId}`,
|
|
195
|
+
status: 404
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// Create HTTP server with explicit error handling
|
|
200
|
+
const server = http.createServer(app);
|
|
201
|
+
server.on('error', (error) => {
|
|
202
|
+
console.error(`[ERROR] HTTP server error: ${error.message}`);
|
|
203
|
+
});
|
|
204
|
+
// Start the server
|
|
205
|
+
server.listen(port, () => {
|
|
206
|
+
console.error(`[MCP] HTTP server listening on port ${port}`);
|
|
207
|
+
console.error(`[MCP] Available endpoints:`);
|
|
208
|
+
console.error(`[MCP] - Health check: http://localhost:${port}/`);
|
|
209
|
+
console.error(`[MCP] - Streamable HTTP: http://localhost:${port}/mcp`);
|
|
210
|
+
console.error(`[MCP] - SSE: http://localhost:${port}/mcp/sse`);
|
|
211
|
+
console.error(`[MCP] - SSE Messages: http://localhost:${port}/mcp/messages`);
|
|
212
|
+
});
|
|
213
|
+
return server;
|
|
214
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ErrorCode, InitializeRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { VERSION, PACKAGE_NAME as SERVER_NAME } from '../version.js';
|
|
6
|
+
import { toolRegistry } from '../tools/index.js';
|
|
7
|
+
import config from '../config/environment.js';
|
|
8
|
+
import { startHttpServer } from './http-server.js';
|
|
9
|
+
import { ResourceHandler } from './resources.js';
|
|
10
|
+
export class GrocyMcpServer {
|
|
11
|
+
server;
|
|
12
|
+
enabledTools = new Set();
|
|
13
|
+
toolSubConfigs = new Map();
|
|
14
|
+
resourceHandler;
|
|
15
|
+
// Expose server instance for HTTP/SSE transport
|
|
16
|
+
get serverInstance() {
|
|
17
|
+
return this.server;
|
|
18
|
+
}
|
|
19
|
+
constructor() {
|
|
20
|
+
this.resourceHandler = new ResourceHandler();
|
|
21
|
+
this.parseToolConfiguration();
|
|
22
|
+
this.setupServer();
|
|
23
|
+
}
|
|
24
|
+
parseToolConfiguration() {
|
|
25
|
+
const { enabledTools, toolSubConfigs } = config.parseToolConfiguration();
|
|
26
|
+
this.toolSubConfigs = toolSubConfigs;
|
|
27
|
+
// Validate tool names against registry
|
|
28
|
+
const validToolNames = new Set(toolRegistry.getToolNames());
|
|
29
|
+
if (enabledTools.size > 0) {
|
|
30
|
+
const invalidTools = Array.from(enabledTools).filter(tool => !validToolNames.has(tool));
|
|
31
|
+
if (invalidTools.length > 0) {
|
|
32
|
+
console.error(`[ERROR] Invalid tool names in configuration: ${invalidTools.join(', ')}`);
|
|
33
|
+
console.error(`[ERROR] Valid tool names are: ${Array.from(validToolNames).sort().join(', ')}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
this.enabledTools = enabledTools;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.error('[CONFIG] No tools enabled - server will have no tools available');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
isToolAllowed(toolName) {
|
|
43
|
+
// Only enabled tools are allowed - all others are disabled by default
|
|
44
|
+
return this.enabledTools.has(toolName);
|
|
45
|
+
}
|
|
46
|
+
async setupServer() {
|
|
47
|
+
this.server = new Server({
|
|
48
|
+
name: SERVER_NAME,
|
|
49
|
+
version: VERSION,
|
|
50
|
+
serverUrl: "https://github.com/miguelangel-nubla/mcp-grocy",
|
|
51
|
+
documentationUrl: "https://github.com/miguelangel-nubla/mcp-grocy/blob/main/README.md"
|
|
52
|
+
}, {
|
|
53
|
+
capabilities: {
|
|
54
|
+
tools: {},
|
|
55
|
+
resources: {},
|
|
56
|
+
prompts: {}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
this.setupHandlers();
|
|
60
|
+
this.setupErrorHandling();
|
|
61
|
+
}
|
|
62
|
+
setupHandlers() {
|
|
63
|
+
// Initialize handler
|
|
64
|
+
this.server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
65
|
+
console.error('[DEBUG] Initialize handler called with request:', JSON.stringify(request));
|
|
66
|
+
return {
|
|
67
|
+
capabilities: {
|
|
68
|
+
tools: {},
|
|
69
|
+
resources: {},
|
|
70
|
+
prompts: {}
|
|
71
|
+
},
|
|
72
|
+
serverInfo: {
|
|
73
|
+
name: SERVER_NAME,
|
|
74
|
+
version: VERSION
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
// Plain initialize handler for compatibility
|
|
79
|
+
const PlainInitializeRequestSchema = z.object({
|
|
80
|
+
jsonrpc: z.literal('2.0'),
|
|
81
|
+
id: z.union([z.string(), z.number()]).optional(),
|
|
82
|
+
method: z.literal('initialize'),
|
|
83
|
+
params: z.any().optional()
|
|
84
|
+
});
|
|
85
|
+
this.server.setRequestHandler(PlainInitializeRequestSchema, async (request) => {
|
|
86
|
+
console.error('[DEBUG] Plain "initialize" handler called');
|
|
87
|
+
let protocolVersion = '2024-11-05';
|
|
88
|
+
if (request.params && typeof request.params.protocolVersion === 'string') {
|
|
89
|
+
protocolVersion = request.params.protocolVersion;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
protocolVersion,
|
|
93
|
+
capabilities: {
|
|
94
|
+
tools: {},
|
|
95
|
+
resources: {},
|
|
96
|
+
prompts: {}
|
|
97
|
+
},
|
|
98
|
+
serverInfo: {
|
|
99
|
+
name: SERVER_NAME,
|
|
100
|
+
version: VERSION
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
// List tools handler
|
|
105
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
106
|
+
const allTools = toolRegistry.getDefinitions();
|
|
107
|
+
const filteredTools = allTools.filter(tool => this.isToolAllowed(tool.name));
|
|
108
|
+
console.error(`[CONFIG] Available tools: ${filteredTools.map(t => t.name).join(', ')}`);
|
|
109
|
+
return { tools: filteredTools };
|
|
110
|
+
});
|
|
111
|
+
// Call tool handler
|
|
112
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
113
|
+
const toolName = request.params.name;
|
|
114
|
+
// Check if the tool is allowed
|
|
115
|
+
if (!this.isToolAllowed(toolName)) {
|
|
116
|
+
throw new McpError(ErrorCode.InvalidRequest, `Tool '${toolName}' is not enabled. Set TOOL__${toolName}=true in your configuration to enable it.`);
|
|
117
|
+
}
|
|
118
|
+
// Get handler from registry
|
|
119
|
+
const handler = toolRegistry.getHandler(toolName);
|
|
120
|
+
if (!handler) {
|
|
121
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const subConfigs = this.toolSubConfigs.get(toolName);
|
|
125
|
+
const result = await handler(request.params.arguments, subConfigs);
|
|
126
|
+
return result; // Cast to proper MCP type
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error(`Error executing tool ${toolName}:`, error);
|
|
130
|
+
if (error instanceof McpError) {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message || error}`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// Resource handlers
|
|
137
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
138
|
+
return this.resourceHandler.listResources();
|
|
139
|
+
});
|
|
140
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
141
|
+
return this.resourceHandler.readResource(request.params.uri);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
setupErrorHandling() {
|
|
145
|
+
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
146
|
+
process.on('SIGINT', async () => {
|
|
147
|
+
await this.server.close();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async start() {
|
|
152
|
+
// Start STDIO transport
|
|
153
|
+
const transport = new StdioServerTransport();
|
|
154
|
+
await this.server.connect(transport);
|
|
155
|
+
console.error('Grocy MCP server running on stdio');
|
|
156
|
+
// Start HTTP/SSE transport if enabled
|
|
157
|
+
const envConfig = config.get();
|
|
158
|
+
if (envConfig.ENABLE_HTTP_SERVER) {
|
|
159
|
+
try {
|
|
160
|
+
console.error(`[CONFIG] Starting HTTP/SSE server on port ${envConfig.HTTP_SERVER_PORT}`);
|
|
161
|
+
const serverFactory = () => new GrocyMcpServer().serverInstance;
|
|
162
|
+
startHttpServer(serverFactory, envConfig.HTTP_SERVER_PORT);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('[ERROR] Failed to start HTTP/SSE server:', error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.error('[CONFIG] HTTP/SSE server is disabled');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export default GrocyMcpServer;
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import apiClient, { ApiError } from '../api/client.js';
|
|
2
|
+
export class BaseToolHandler {
|
|
3
|
+
safeJsonStringify(data) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.stringify(data, null, 2);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
console.error('Error stringifying JSON:', error);
|
|
9
|
+
return JSON.stringify({ error: 'Error formatting response data' }, null, 2);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
createSuccessResult(data) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{
|
|
15
|
+
type: 'text',
|
|
16
|
+
text: this.safeJsonStringify(data)
|
|
17
|
+
}]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
createErrorResult(error, context) {
|
|
21
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
22
|
+
const result = { error: errorMessage };
|
|
23
|
+
if (context) {
|
|
24
|
+
result.context = context;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
content: [{
|
|
28
|
+
type: 'text',
|
|
29
|
+
text: this.safeJsonStringify(result)
|
|
30
|
+
}],
|
|
31
|
+
isError: true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async handleApiCall(endpoint, description, options = {}) {
|
|
35
|
+
try {
|
|
36
|
+
const method = options.method || 'GET';
|
|
37
|
+
const body = options.body || null;
|
|
38
|
+
const headers = options.headers || {};
|
|
39
|
+
const queryParams = options.queryParams || {};
|
|
40
|
+
const response = await apiClient.request(endpoint, {
|
|
41
|
+
method,
|
|
42
|
+
body,
|
|
43
|
+
headers,
|
|
44
|
+
queryParams
|
|
45
|
+
});
|
|
46
|
+
return this.createSuccessResult(response.data);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error(`Error in ${description}:`, error);
|
|
50
|
+
if (error instanceof ApiError) {
|
|
51
|
+
return this.createErrorResult(`Failed to ${description.toLowerCase()}: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
return this.createErrorResult(`Failed to ${description.toLowerCase()}: ${error.message || error}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|