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,8 +1,8 @@
|
|
|
1
1
|
# Grocy API Response Format Documentation
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The dev tool **`system_dev_test_request`** returns a JSON payload with request details, response information, and validation results. Other tools (e.g. `inventory_stock_get_all`, `shopping_list_add_item`) return the Grocy API body as text content (typically stringified JSON).
|
|
4
4
|
|
|
5
|
-
## `
|
|
5
|
+
## `system_dev_test_request` tool response structure
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
@@ -29,7 +29,7 @@ The Grocy API testing tool (`test_request`) returns a comprehensive JSON respons
|
|
|
29
29
|
"id": "1",
|
|
30
30
|
"name": "Cookies",
|
|
31
31
|
"description": null,
|
|
32
|
-
"product_group_id": "1"
|
|
32
|
+
"product_group_id": "1"
|
|
33
33
|
// ... other product fields ...
|
|
34
34
|
}
|
|
35
35
|
},
|
|
@@ -72,9 +72,10 @@ GROCY_API_KEY=your-private-api-key
|
|
|
72
72
|
|
|
73
73
|
These values can be set in your `.env` file for local development or in your project configuration for production use.
|
|
74
74
|
|
|
75
|
-
## Response Fields for `
|
|
75
|
+
## Response Fields for `system_dev_test_request`
|
|
76
76
|
|
|
77
77
|
### Request Details (`request`)
|
|
78
|
+
|
|
78
79
|
- `url`: Full URL of the Grocy API endpoint called, including base URL and path.
|
|
79
80
|
- `method`: HTTP method used (e.g., GET, POST, PUT, DELETE).
|
|
80
81
|
- `headers`: Request headers sent to the Grocy API. Sensitive headers like `GROCY-API-KEY` will have their values redacted.
|
|
@@ -82,6 +83,7 @@ These values can be set in your `.env` file for local development or in your pro
|
|
|
82
83
|
- `authMethod`: Authentication method used. For Grocy, this will typically be `apikey` if `GROCY_API_KEY` is configured, or `none`.
|
|
83
84
|
|
|
84
85
|
### Response Details (`response`)
|
|
86
|
+
|
|
85
87
|
- `statusCode`: HTTP status code returned by the Grocy API (e.g., 200, 400, 401).
|
|
86
88
|
- `statusText`: HTTP status message (e.g., "OK", "Bad Request").
|
|
87
89
|
- `timing`: Duration of the API request in milliseconds.
|
|
@@ -89,6 +91,7 @@ These values can be set in your `.env` file for local development or in your pro
|
|
|
89
91
|
- `body`: Response body content from the Grocy API. This will be the JSON data returned by Grocy.
|
|
90
92
|
|
|
91
93
|
### Validation (`validation`)
|
|
94
|
+
|
|
92
95
|
- `isError`: Boolean, `true` if the HTTP status code is 400 or higher, indicating an error.
|
|
93
96
|
- `messages`: Array of messages, including success messages or error details.
|
|
94
97
|
- `truncated` (optional): If the response body exceeds `REST_RESPONSE_SIZE_LIMIT`, this object will contain details about the truncation.
|
|
@@ -99,9 +102,10 @@ These values can be set in your `.env` file for local development or in your pro
|
|
|
99
102
|
|
|
100
103
|
## Specialized Grocy Tools Response Format
|
|
101
104
|
|
|
102
|
-
Tools like `
|
|
105
|
+
Tools like `inventory_stock_get_all`, `inventory_products_get`, `shopping_list_add_item`, etc., return the Grocy API response as MCP text content (usually stringified JSON).
|
|
106
|
+
|
|
107
|
+
Example shape for a product-related read (illustrative):
|
|
103
108
|
|
|
104
|
-
Example for `get_product` (if it existed as a specialized tool for a single product):
|
|
105
109
|
```json
|
|
106
110
|
{
|
|
107
111
|
"content": [
|
|
@@ -119,6 +123,7 @@ Example for `get_product` (if it existed as a specialized tool for a single prod
|
|
|
119
123
|
```
|
|
120
124
|
|
|
121
125
|
If an error occurs with a specialized tool, the response will typically look like:
|
|
126
|
+
|
|
122
127
|
```json
|
|
123
128
|
{
|
|
124
129
|
"content": [
|
|
@@ -131,9 +136,10 @@ If an error occurs with a specialized tool, the response will typically look lik
|
|
|
131
136
|
}
|
|
132
137
|
```
|
|
133
138
|
|
|
134
|
-
## Error
|
|
139
|
+
## Error response example for `system_dev_test_request`
|
|
140
|
+
|
|
141
|
+
If **`system_dev_test_request`** encounters an API error (e.g., authentication failure):
|
|
135
142
|
|
|
136
|
-
If the `test_request` tool encounters an API error (e.g., authentication failure):
|
|
137
143
|
```json
|
|
138
144
|
{
|
|
139
145
|
"request": {
|
|
@@ -149,17 +155,16 @@ If the `test_request` tool encounters an API error (e.g., authentication failure
|
|
|
149
155
|
"statusCode": 401,
|
|
150
156
|
"statusText": "Unauthorized",
|
|
151
157
|
"timing": "50ms",
|
|
152
|
-
"headers": {
|
|
158
|
+
"headers": {
|
|
159
|
+
/* ... headers ... */
|
|
160
|
+
},
|
|
153
161
|
"body": {
|
|
154
162
|
"error_message": "API key is missing or invalid."
|
|
155
163
|
}
|
|
156
164
|
},
|
|
157
165
|
"validation": {
|
|
158
166
|
"isError": true,
|
|
159
|
-
"messages": [
|
|
160
|
-
"Request failed with status 401",
|
|
161
|
-
"API key is missing or invalid."
|
|
162
|
-
]
|
|
167
|
+
"messages": ["Request failed with status 401", "API key is missing or invalid."]
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
```
|
|
@@ -1,39 +1,106 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
2
|
+
import { randomUUID, timingSafeEqual } from 'crypto';
|
|
3
3
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
4
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
5
5
|
import { VERSION, PACKAGE_NAME as SERVER_NAME } from '../version.js';
|
|
6
6
|
import cors from 'cors';
|
|
7
7
|
import http from 'http';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
|
+
/** Constant-time string comparison to prevent timing attacks on token validation. */
|
|
10
|
+
function safeEqual(a, b) {
|
|
11
|
+
if (a.length !== b.length)
|
|
12
|
+
return false;
|
|
13
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
14
|
+
}
|
|
15
|
+
function createMcpAccessGate(accessToken) {
|
|
16
|
+
return (req, res, next) => {
|
|
17
|
+
if (!accessToken) {
|
|
18
|
+
next();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (req.method === 'OPTIONS') {
|
|
22
|
+
next();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const bearer = req.headers.authorization;
|
|
26
|
+
const headerToken = req.headers['x-mcp-access-token'];
|
|
27
|
+
const headerTokenStr = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
28
|
+
const q = req.query.access_token;
|
|
29
|
+
const queryToken = typeof q === 'string' ? q : undefined;
|
|
30
|
+
const ok = (typeof bearer === 'string' && safeEqual(bearer, `Bearer ${accessToken}`)) ||
|
|
31
|
+
(typeof headerTokenStr === 'string' && safeEqual(headerTokenStr, accessToken)) ||
|
|
32
|
+
(req.method === 'GET' &&
|
|
33
|
+
typeof queryToken === 'string' &&
|
|
34
|
+
safeEqual(queryToken, accessToken));
|
|
35
|
+
if (ok) {
|
|
36
|
+
next();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
res.status(401).json({
|
|
40
|
+
jsonrpc: '2.0',
|
|
41
|
+
error: {
|
|
42
|
+
code: -32000,
|
|
43
|
+
message: 'Unauthorized: configure MCP_HTTP_ACCESS_TOKEN and send Authorization: Bearer, X-MCP-Access-Token, or access_token query (GET only)',
|
|
44
|
+
},
|
|
45
|
+
id: (req.body && typeof req.body === 'object' && 'id' in req.body
|
|
46
|
+
? req.body.id
|
|
47
|
+
: null) ?? null,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
}
|
|
9
51
|
// HTTP Transport for MCP (Context7 style)
|
|
10
|
-
export function startHttpServer(mcpServer, port = 8080) {
|
|
52
|
+
export function startHttpServer(mcpServer, port = 8080, security) {
|
|
11
53
|
return new Promise((resolve, reject) => {
|
|
12
54
|
const app = express();
|
|
55
|
+
const mcpAccessGate = createMcpAccessGate(security.accessToken);
|
|
56
|
+
if (security.accessToken) {
|
|
57
|
+
logger.config('HTTP MCP access token is enabled (Bearer, X-MCP-Access-Token, or access_token on GET)');
|
|
58
|
+
}
|
|
13
59
|
// Enable JSON body parsing with increased limit
|
|
14
60
|
app.use(express.json({
|
|
15
|
-
limit: '10mb'
|
|
61
|
+
limit: '10mb',
|
|
16
62
|
}));
|
|
17
|
-
// Enable CORS for all routes
|
|
18
63
|
app.use(cors({
|
|
19
|
-
origin:
|
|
20
|
-
methods: ['GET', 'POST', 'OPTIONS'],
|
|
21
|
-
allowedHeaders: [
|
|
64
|
+
origin: security.corsOrigin,
|
|
65
|
+
methods: ['GET', 'POST', 'OPTIONS', 'DELETE'],
|
|
66
|
+
allowedHeaders: [
|
|
67
|
+
'Origin',
|
|
68
|
+
'X-Requested-With',
|
|
69
|
+
'Content-Type',
|
|
70
|
+
'Accept',
|
|
71
|
+
'Mcp-Session-Id',
|
|
72
|
+
'Mcp-Protocol-Version',
|
|
73
|
+
'Authorization',
|
|
74
|
+
'X-MCP-Access-Token',
|
|
75
|
+
],
|
|
22
76
|
exposedHeaders: ['Mcp-Session-Id', 'Content-Type'],
|
|
23
|
-
optionsSuccessStatus: 200
|
|
77
|
+
optionsSuccessStatus: 200,
|
|
24
78
|
}));
|
|
25
|
-
//
|
|
26
|
-
|
|
79
|
+
// Health check endpoint — always accessible for monitoring/load balancers,
|
|
80
|
+
// but only exposes service details when no access token is configured or request is authenticated.
|
|
81
|
+
app.get('/', (req, res) => {
|
|
82
|
+
const minimal = { status: 'ok' };
|
|
83
|
+
if (security.accessToken) {
|
|
84
|
+
const bearer = req.headers.authorization;
|
|
85
|
+
const headerToken = req.headers['x-mcp-access-token'];
|
|
86
|
+
const headerTokenStr = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
87
|
+
const authenticated = (typeof bearer === 'string' && safeEqual(bearer, `Bearer ${security.accessToken}`)) ||
|
|
88
|
+
(typeof headerTokenStr === 'string' && safeEqual(headerTokenStr, security.accessToken));
|
|
89
|
+
if (!authenticated) {
|
|
90
|
+
res.json(minimal);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
27
94
|
res.json({
|
|
28
|
-
|
|
95
|
+
...minimal,
|
|
29
96
|
service: SERVER_NAME,
|
|
30
97
|
version: VERSION,
|
|
31
98
|
message: 'MCP server is running',
|
|
32
99
|
endpoints: {
|
|
33
100
|
streamable: '/mcp',
|
|
34
101
|
sse: '/mcp/sse',
|
|
35
|
-
sseMessages: '/mcp/messages'
|
|
36
|
-
}
|
|
102
|
+
sseMessages: '/mcp/messages',
|
|
103
|
+
},
|
|
37
104
|
});
|
|
38
105
|
});
|
|
39
106
|
// Session management for transports
|
|
@@ -55,40 +122,50 @@ export function startHttpServer(mcpServer, port = 8080) {
|
|
|
55
122
|
}
|
|
56
123
|
};
|
|
57
124
|
// Streamable HTTP endpoint (Context7 modern)
|
|
58
|
-
app.
|
|
125
|
+
app.all('/mcp', mcpAccessGate, async (req, res) => {
|
|
59
126
|
try {
|
|
60
|
-
const clientSessionId = req.headers['mcp-session-id']
|
|
127
|
+
const clientSessionId = req.headers['mcp-session-id'] ||
|
|
128
|
+
req.query.sessionId ||
|
|
129
|
+
req.query['mcp-session-id'];
|
|
61
130
|
let transport = undefined;
|
|
62
131
|
// Accept header check (can be done early)
|
|
63
132
|
const accept = req.headers.accept || '';
|
|
133
|
+
// Home Assistant might not send perfect accept headers, so we log a warning instead of blocking
|
|
64
134
|
if (!accept.includes('application/json') && !accept.includes('text/event-stream')) {
|
|
65
|
-
logger.
|
|
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;
|
|
135
|
+
logger.warn('Client should accept application/json or text/event-stream', 'server');
|
|
75
136
|
}
|
|
76
|
-
|
|
137
|
+
const isInitializeRequest = req.method === 'POST' && req.body?.method === 'initialize';
|
|
138
|
+
if (clientSessionId && !isInitializeRequest) {
|
|
139
|
+
// Existing session: look up transport
|
|
77
140
|
transport = streamableTransports[clientSessionId];
|
|
78
141
|
if (!transport) {
|
|
142
|
+
logger.warn(`Session ${clientSessionId} not found, returning 400`, 'server');
|
|
79
143
|
res.status(400).json({
|
|
80
144
|
jsonrpc: '2.0',
|
|
81
|
-
error: {
|
|
82
|
-
|
|
145
|
+
error: {
|
|
146
|
+
code: -32001,
|
|
147
|
+
message: `Invalid or expired session ID: ${clientSessionId}. Please re-initialize.`,
|
|
148
|
+
},
|
|
149
|
+
id: req.body?.id || null,
|
|
83
150
|
});
|
|
84
151
|
return;
|
|
85
152
|
}
|
|
86
153
|
}
|
|
87
|
-
else {
|
|
88
|
-
//
|
|
154
|
+
else if (isInitializeRequest) {
|
|
155
|
+
// POST initialize: create new transport and session
|
|
156
|
+
if (clientSessionId) {
|
|
157
|
+
logger.info(`Initialize request included prior session ID ${clientSessionId} (non-graceful reconnect), starting fresh session`, 'server');
|
|
158
|
+
// Clean up the stale transport to prevent memory leaks
|
|
159
|
+
const staleTransport = streamableTransports[clientSessionId];
|
|
160
|
+
if (staleTransport) {
|
|
161
|
+
delete streamableTransports[clientSessionId];
|
|
162
|
+
staleTransport.close?.().catch(() => { });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
89
165
|
const newGeneratedSessionId = randomUUID();
|
|
90
166
|
const newTransportInstance = new StreamableHTTPServerTransport({
|
|
91
|
-
sessionIdGenerator: () => newGeneratedSessionId
|
|
167
|
+
sessionIdGenerator: () => newGeneratedSessionId,
|
|
168
|
+
enableJsonResponse: true,
|
|
92
169
|
});
|
|
93
170
|
transport = newTransportInstance;
|
|
94
171
|
streamableTransports[newGeneratedSessionId] = transport;
|
|
@@ -97,11 +174,31 @@ export function startHttpServer(mcpServer, port = 8080) {
|
|
|
97
174
|
delete streamableTransports[closedSessionId];
|
|
98
175
|
};
|
|
99
176
|
const serverInstance = getServerInstance();
|
|
100
|
-
await serverInstance.connect(transport);
|
|
177
|
+
await serverInstance.connect(transport);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Non-initialize request without a session ID (e.g. GET SSE before session established)
|
|
181
|
+
// Return 405 so clients that probe for SSE support handle it gracefully
|
|
182
|
+
res
|
|
183
|
+
.status(405)
|
|
184
|
+
.set('Allow', 'POST')
|
|
185
|
+
.json({
|
|
186
|
+
jsonrpc: '2.0',
|
|
187
|
+
error: {
|
|
188
|
+
code: -32000,
|
|
189
|
+
message: 'Method not allowed: session not established. Send a POST initialize request first.',
|
|
190
|
+
},
|
|
191
|
+
id: req.body?.id || null,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
101
194
|
}
|
|
102
195
|
if (!transport) {
|
|
103
196
|
logger.error('Transport is undefined before handling request. This should not happen.', 'server');
|
|
104
|
-
res.status(500).json({
|
|
197
|
+
res.status(500).json({
|
|
198
|
+
jsonrpc: '2.0',
|
|
199
|
+
error: { code: -32000, message: 'Internal server error: Transport not available' },
|
|
200
|
+
id: req.body?.id || null,
|
|
201
|
+
});
|
|
105
202
|
return;
|
|
106
203
|
}
|
|
107
204
|
if (transport.sessionId) {
|
|
@@ -117,15 +214,15 @@ export function startHttpServer(mcpServer, port = 8080) {
|
|
|
117
214
|
jsonrpc: '2.0',
|
|
118
215
|
error: {
|
|
119
216
|
code: -32000,
|
|
120
|
-
message: `Internal server error: ${error instanceof Error ? error.message : String(error)}
|
|
217
|
+
message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,
|
|
121
218
|
},
|
|
122
|
-
id: req.body?.id
|
|
219
|
+
id: req.body?.id ?? null,
|
|
123
220
|
});
|
|
124
221
|
}
|
|
125
222
|
}
|
|
126
223
|
});
|
|
127
224
|
// SSE endpoint
|
|
128
|
-
app.get('/mcp/sse', async (_req, res) => {
|
|
225
|
+
app.get('/mcp/sse', mcpAccessGate, async (_req, res) => {
|
|
129
226
|
try {
|
|
130
227
|
// Set SSE headers before creating transport
|
|
131
228
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
@@ -133,6 +230,7 @@ export function startHttpServer(mcpServer, port = 8080) {
|
|
|
133
230
|
res.setHeader('Connection', 'keep-alive');
|
|
134
231
|
const transport = new SSEServerTransport('/mcp/messages', res);
|
|
135
232
|
const sessionId = transport.sessionId;
|
|
233
|
+
logger.info(`Created SSE session: ${sessionId}`, 'DEBUG');
|
|
136
234
|
sseTransports[sessionId] = transport;
|
|
137
235
|
// Handle connection cleanup
|
|
138
236
|
const cleanup = () => {
|
|
@@ -149,7 +247,9 @@ export function startHttpServer(mcpServer, port = 8080) {
|
|
|
149
247
|
sseServerInstances[sessionId] = isolatedServer;
|
|
150
248
|
// Connect transport to isolated MCP server (non-blocking)
|
|
151
249
|
isolatedServer.connect(transport).catch((error) => {
|
|
152
|
-
logger.error(`Failed to connect SSE transport for session ${sessionId}`, 'server', {
|
|
250
|
+
logger.error(`Failed to connect SSE transport for session ${sessionId}`, 'server', {
|
|
251
|
+
error,
|
|
252
|
+
});
|
|
153
253
|
cleanup();
|
|
154
254
|
if (!res.headersSent) {
|
|
155
255
|
res.status(500).end();
|
|
@@ -169,35 +269,47 @@ export function startHttpServer(mcpServer, port = 8080) {
|
|
|
169
269
|
}
|
|
170
270
|
});
|
|
171
271
|
// Message endpoint for SSE
|
|
172
|
-
app.post('/mcp/messages', async (req, res) => {
|
|
272
|
+
app.post('/mcp/messages', mcpAccessGate, async (req, res) => {
|
|
173
273
|
const sessionId = req.query.sessionId;
|
|
174
274
|
if (!sessionId) {
|
|
275
|
+
logger.error('Missing sessionId parameter in SSE message', 'server');
|
|
175
276
|
res.status(400).json({
|
|
176
277
|
error: 'Missing sessionId parameter',
|
|
177
|
-
status: 400
|
|
278
|
+
status: 400,
|
|
178
279
|
});
|
|
179
280
|
return;
|
|
180
281
|
}
|
|
181
282
|
const transport = sseTransports[sessionId];
|
|
182
283
|
if (transport) {
|
|
284
|
+
logger.info(`Found active transport for session ${sessionId}`, 'DEBUG');
|
|
183
285
|
try {
|
|
184
286
|
await transport.handlePostMessage(req, res, req.body);
|
|
185
287
|
}
|
|
186
288
|
catch (error) {
|
|
187
|
-
logger.error(`Failed to handle SSE message for session ${sessionId}`, 'server', {
|
|
289
|
+
logger.error(`Failed to handle SSE message for session ${sessionId}`, 'server', {
|
|
290
|
+
error,
|
|
291
|
+
});
|
|
188
292
|
res.status(500).json({
|
|
189
293
|
error: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,
|
|
190
|
-
status: 500
|
|
294
|
+
status: 500,
|
|
191
295
|
});
|
|
192
296
|
}
|
|
193
297
|
}
|
|
194
298
|
else {
|
|
195
299
|
res.status(404).json({
|
|
196
300
|
error: `No active SSE connection found for session ID: ${sessionId}`,
|
|
197
|
-
status: 404
|
|
301
|
+
status: 404,
|
|
198
302
|
});
|
|
199
303
|
}
|
|
200
304
|
});
|
|
305
|
+
// Catch-all for undefined routes — return JSON instead of Express default HTML
|
|
306
|
+
app.use((_req, res) => {
|
|
307
|
+
res.status(404).json({
|
|
308
|
+
jsonrpc: '2.0',
|
|
309
|
+
error: { code: -32000, message: 'Not found' },
|
|
310
|
+
id: null,
|
|
311
|
+
});
|
|
312
|
+
});
|
|
201
313
|
// Create HTTP server with explicit error handling
|
|
202
314
|
const server = http.createServer(app);
|
|
203
315
|
server.on('error', (error) => {
|