shabaaspay-mcp-server 1.0.1
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 +21 -0
- package/dist/api/client.d.ts +60 -0
- package/dist/api/client.js +214 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +79 -0
- package/dist/enricher/action-suggester.d.ts +2 -0
- package/dist/enricher/action-suggester.js +26 -0
- package/dist/enricher/index.d.ts +2 -0
- package/dist/enricher/index.js +166 -0
- package/dist/enricher/status-analyzer.d.ts +6 -0
- package/dist/enricher/status-analyzer.js +71 -0
- package/dist/enricher/summary-generator.d.ts +8 -0
- package/dist/enricher/summary-generator.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -0
- package/dist/security/auth.d.ts +4 -0
- package/dist/security/auth.js +30 -0
- package/dist/security/policy.d.ts +18 -0
- package/dist/security/policy.js +35 -0
- package/dist/security/rate-limiter.d.ts +13 -0
- package/dist/security/rate-limiter.js +55 -0
- package/dist/security/validator.d.ts +6 -0
- package/dist/security/validator.js +22 -0
- package/dist/server/http-server.d.ts +28 -0
- package/dist/server/http-server.js +524 -0
- package/dist/server/stdio-server.d.ts +13 -0
- package/dist/server/stdio-server.js +114 -0
- package/dist/server-http.d.ts +2 -0
- package/dist/server-http.js +27 -0
- package/dist/tools/auth.d.ts +17 -0
- package/dist/tools/auth.js +51 -0
- package/dist/tools/index.d.ts +159 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/payment-agreements.d.ts +68 -0
- package/dist/tools/payment-agreements.js +92 -0
- package/dist/tools/payment-initiations.d.ts +84 -0
- package/dist/tools/payment-initiations.js +162 -0
- package/dist/tools/response-helpers.d.ts +4 -0
- package/dist/tools/response-helpers.js +32 -0
- package/dist/types/index.d.ts +184 -0
- package/dist/types/index.js +80 -0
- package/dist/worker.d.ts +15 -0
- package/dist/worker.js +767 -0
- package/package.json +64 -0
- package/readme.md +113 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HttpMcpServer = void 0;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const url_1 = require("url");
|
|
9
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
10
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
11
|
+
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
12
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
13
|
+
const client_js_1 = require("../api/client.js");
|
|
14
|
+
const index_js_2 = require("../tools/index.js");
|
|
15
|
+
const rate_limiter_js_1 = require("../security/rate-limiter.js");
|
|
16
|
+
const auth_js_1 = require("../security/auth.js");
|
|
17
|
+
const policy_js_1 = require("../security/policy.js");
|
|
18
|
+
class HttpMcpServer {
|
|
19
|
+
server;
|
|
20
|
+
tools;
|
|
21
|
+
rateLimiter;
|
|
22
|
+
config;
|
|
23
|
+
mcpServer;
|
|
24
|
+
mcpTransport;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
// Initialize API client
|
|
28
|
+
const apiClient = new client_js_1.ShabaasApiClient(config);
|
|
29
|
+
// Create tools
|
|
30
|
+
this.tools = (0, index_js_2.createAllTools)(apiClient, config);
|
|
31
|
+
// Create MCP server (Streamable HTTP transport for VS Code / MCP HTTP clients)
|
|
32
|
+
this.mcpServer = new index_js_1.Server({
|
|
33
|
+
name: 'shabaaspay-mcp',
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
}, {
|
|
36
|
+
capabilities: {
|
|
37
|
+
tools: {},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
this.setupMcpHandlers();
|
|
41
|
+
this.mcpTransport = new streamableHttp_js_1.StreamableHTTPServerTransport();
|
|
42
|
+
// Initialize rate limiter
|
|
43
|
+
this.rateLimiter = new rate_limiter_js_1.RateLimiter(config.rateLimitPerMinute, config.rateLimitPerHour);
|
|
44
|
+
// Create HTTP server
|
|
45
|
+
this.server = http_1.default.createServer(this.handleRequest.bind(this));
|
|
46
|
+
}
|
|
47
|
+
async handleRequest(req, res) {
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
const requestId = `req_${startTime}`;
|
|
50
|
+
try {
|
|
51
|
+
// Parse request
|
|
52
|
+
const parsedUrl = new url_1.URL(req.url || '', `http://${req.headers.host}`);
|
|
53
|
+
const path = parsedUrl.pathname;
|
|
54
|
+
// CORS preflight for any route (including /mcp)
|
|
55
|
+
if ((req.method || '').toUpperCase() === 'OPTIONS') {
|
|
56
|
+
this.sendResponse(res, this.corsResponse());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Dedicated MCP HTTP transport endpoint
|
|
60
|
+
if (path === '/mcp') {
|
|
61
|
+
await this.handleMcpTransport(req, res, startTime);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const body = await this.parseBody(req);
|
|
65
|
+
const request = {
|
|
66
|
+
method: req.method || 'GET',
|
|
67
|
+
path,
|
|
68
|
+
headers: req.headers,
|
|
69
|
+
body,
|
|
70
|
+
requestId,
|
|
71
|
+
};
|
|
72
|
+
console.log(`[HTTP] ${request.method} ${request.path}`);
|
|
73
|
+
// Health check does not require auth
|
|
74
|
+
if (request.method === 'GET' && request.path === '/health') {
|
|
75
|
+
this.sendResponse(res, this.healthResponse());
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Handle CORS preflight
|
|
79
|
+
if (request.method === 'OPTIONS') {
|
|
80
|
+
this.sendResponse(res, this.corsResponse());
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Authenticate
|
|
84
|
+
const authResult = this.authenticate(request);
|
|
85
|
+
if (!authResult.success) {
|
|
86
|
+
this.sendResponse(res, authResult.response);
|
|
87
|
+
this.logStructured({
|
|
88
|
+
requestId,
|
|
89
|
+
clientId: undefined,
|
|
90
|
+
toolName: request.toolName,
|
|
91
|
+
statusCode: authResult.response?.statusCode || 401,
|
|
92
|
+
latencyMs: Date.now() - startTime,
|
|
93
|
+
rejection: 'auth_failed',
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
request.clientId = authResult.clientId;
|
|
98
|
+
// Rate limiting
|
|
99
|
+
const clientId = this.getClientIdentifier(request);
|
|
100
|
+
const minuteLimit = this.rateLimiter.checkLimit(clientId, 'minute');
|
|
101
|
+
const hourLimit = this.rateLimiter.checkLimit(clientId, 'hour');
|
|
102
|
+
if (!minuteLimit.allowed) {
|
|
103
|
+
this.sendResponse(res, this.rateLimitResponse(minuteLimit));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!hourLimit.allowed) {
|
|
107
|
+
this.sendResponse(res, this.rateLimitResponse(hourLimit));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Route request
|
|
111
|
+
const response = await this.routeRequest(request);
|
|
112
|
+
// Add rate limit headers
|
|
113
|
+
response.headers['X-RateLimit-Limit-Minute'] = minuteLimit.limit.toString();
|
|
114
|
+
response.headers['X-RateLimit-Remaining-Minute'] = minuteLimit.remaining.toString();
|
|
115
|
+
response.headers['X-RateLimit-Reset-Minute'] = minuteLimit.reset.toString();
|
|
116
|
+
response.headers['X-RateLimit-Limit-Hour'] = hourLimit.limit.toString();
|
|
117
|
+
response.headers['X-RateLimit-Remaining-Hour'] = hourLimit.remaining.toString();
|
|
118
|
+
response.headers['X-RateLimit-Reset-Hour'] = hourLimit.reset.toString();
|
|
119
|
+
// Add processing time
|
|
120
|
+
response.headers['X-Processing-Time'] = `${Date.now() - startTime}ms`;
|
|
121
|
+
this.sendResponse(res, response, requestId);
|
|
122
|
+
this.logStructured({
|
|
123
|
+
requestId,
|
|
124
|
+
clientId: request.clientId,
|
|
125
|
+
toolName: request.toolName,
|
|
126
|
+
statusCode: response.statusCode,
|
|
127
|
+
latencyMs: Date.now() - startTime,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error('[HTTP] Error:', error);
|
|
132
|
+
this.sendResponse(res, this.errorResponse('Internal error'), requestId);
|
|
133
|
+
this.logStructured({
|
|
134
|
+
requestId,
|
|
135
|
+
clientId: undefined,
|
|
136
|
+
toolName: undefined,
|
|
137
|
+
statusCode: 500,
|
|
138
|
+
latencyMs: Date.now() - startTime,
|
|
139
|
+
rejection: 'internal_error',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
healthResponse() {
|
|
144
|
+
return {
|
|
145
|
+
statusCode: 200,
|
|
146
|
+
headers: this.corsHeaders(),
|
|
147
|
+
body: {
|
|
148
|
+
status: 'healthy',
|
|
149
|
+
version: '1.0.0',
|
|
150
|
+
environment: this.config.environment,
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async handleMcpTransport(req, res, startTime) {
|
|
156
|
+
console.log(`[HTTP][MCP] ${req.method} ${req.url}`);
|
|
157
|
+
// Always set CORS headers for MCP responses
|
|
158
|
+
const baseCors = this.corsHeaders();
|
|
159
|
+
Object.entries(baseCors).forEach(([k, v]) => res.setHeader(k, v));
|
|
160
|
+
// Auth
|
|
161
|
+
const authResult = this.authenticateHeaders(req.headers);
|
|
162
|
+
if (!authResult.success) {
|
|
163
|
+
this.sendResponse(res, authResult.response);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Rate limiting
|
|
167
|
+
const clientId = this.getClientIdentifierFromHeaders(req.headers);
|
|
168
|
+
const minuteLimit = this.rateLimiter.checkLimit(clientId, 'minute');
|
|
169
|
+
const hourLimit = this.rateLimiter.checkLimit(clientId, 'hour');
|
|
170
|
+
if (!minuteLimit.allowed) {
|
|
171
|
+
this.sendResponse(res, this.rateLimitResponse(minuteLimit));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!hourLimit.allowed) {
|
|
175
|
+
this.sendResponse(res, this.rateLimitResponse(hourLimit));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Pass through to MCP transport
|
|
179
|
+
res.setHeader('X-RateLimit-Limit-Minute', minuteLimit.limit.toString());
|
|
180
|
+
res.setHeader('X-RateLimit-Remaining-Minute', minuteLimit.remaining.toString());
|
|
181
|
+
res.setHeader('X-RateLimit-Reset-Minute', minuteLimit.reset.toString());
|
|
182
|
+
res.setHeader('X-RateLimit-Limit-Hour', hourLimit.limit.toString());
|
|
183
|
+
res.setHeader('X-RateLimit-Remaining-Hour', hourLimit.remaining.toString());
|
|
184
|
+
res.setHeader('X-RateLimit-Reset-Hour', hourLimit.reset.toString());
|
|
185
|
+
res.setHeader('X-Processing-Time', `${Date.now() - startTime}ms`);
|
|
186
|
+
await this.mcpTransport.handleRequest(req, res);
|
|
187
|
+
}
|
|
188
|
+
async parseBody(req) {
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
let body = '';
|
|
191
|
+
req.on('data', chunk => (body += chunk.toString()));
|
|
192
|
+
req.on('end', () => {
|
|
193
|
+
try {
|
|
194
|
+
resolve(body ? JSON.parse(body) : undefined);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
reject(new Error('Invalid JSON body'));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
req.on('error', reject);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
authenticate(request) {
|
|
204
|
+
const token = (0, auth_js_1.extractBearerToken)(request.headers.authorization);
|
|
205
|
+
if (!token) {
|
|
206
|
+
return {
|
|
207
|
+
success: false,
|
|
208
|
+
response: {
|
|
209
|
+
statusCode: 401,
|
|
210
|
+
headers: this.corsHeaders(),
|
|
211
|
+
body: {
|
|
212
|
+
error: 'Authentication required',
|
|
213
|
+
message: 'Authorization header with UUID is required',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Optional HTTP guard
|
|
219
|
+
if (this.config.mcpHttpApiKey && token !== this.config.mcpHttpApiKey) {
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
response: {
|
|
223
|
+
statusCode: 403,
|
|
224
|
+
headers: this.corsHeaders(),
|
|
225
|
+
body: {
|
|
226
|
+
error: 'Invalid API key',
|
|
227
|
+
message: 'The provided API key is not valid',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const policyResult = (0, policy_js_1.lookupClientPolicy)(token, this.config.environment);
|
|
233
|
+
if (!policyResult.policy) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
response: {
|
|
237
|
+
statusCode: 403,
|
|
238
|
+
headers: this.corsHeaders(),
|
|
239
|
+
body: {
|
|
240
|
+
error: 'Forbidden',
|
|
241
|
+
message: 'Access denied',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return { success: true, clientId: policyResult.policy.client_id };
|
|
247
|
+
}
|
|
248
|
+
authenticateHeaders(headers) {
|
|
249
|
+
const token = (0, auth_js_1.extractBearerToken)(headers.authorization);
|
|
250
|
+
if (!token) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
response: {
|
|
254
|
+
statusCode: 401,
|
|
255
|
+
headers: this.corsHeaders(),
|
|
256
|
+
body: {
|
|
257
|
+
error: 'Authentication required',
|
|
258
|
+
message: 'Authorization header with UUID is required',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (this.config.mcpHttpApiKey && token !== this.config.mcpHttpApiKey) {
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
response: {
|
|
267
|
+
statusCode: 403,
|
|
268
|
+
headers: this.corsHeaders(),
|
|
269
|
+
body: {
|
|
270
|
+
error: 'Invalid API key',
|
|
271
|
+
message: 'The provided API key is not valid',
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const policyResult = (0, policy_js_1.lookupClientPolicy)(token, this.config.environment);
|
|
277
|
+
if (!policyResult.policy) {
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
response: {
|
|
281
|
+
statusCode: 403,
|
|
282
|
+
headers: this.corsHeaders(),
|
|
283
|
+
body: {
|
|
284
|
+
error: 'Forbidden',
|
|
285
|
+
message: 'Access denied',
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return { success: true, clientId: policyResult.policy.client_id };
|
|
291
|
+
}
|
|
292
|
+
getClientIdentifier(request) {
|
|
293
|
+
return this.getClientIdentifierFromHeaders(request.headers);
|
|
294
|
+
}
|
|
295
|
+
getClientIdentifierFromHeaders(headers) {
|
|
296
|
+
// Use IP address for rate limiting
|
|
297
|
+
return headers['x-forwarded-for']?.toString() ||
|
|
298
|
+
headers['x-real-ip']?.toString() ||
|
|
299
|
+
'unknown';
|
|
300
|
+
}
|
|
301
|
+
async routeRequest(request) {
|
|
302
|
+
const { method, path, body } = request;
|
|
303
|
+
// Health check
|
|
304
|
+
if (path === '/health' && method === 'GET') {
|
|
305
|
+
return {
|
|
306
|
+
statusCode: 200,
|
|
307
|
+
headers: this.corsHeaders(),
|
|
308
|
+
body: {
|
|
309
|
+
status: 'healthy',
|
|
310
|
+
version: '1.0.0',
|
|
311
|
+
environment: this.config.environment,
|
|
312
|
+
timestamp: new Date().toISOString(),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// List tools
|
|
317
|
+
if (path === '/tools' && method === 'GET') {
|
|
318
|
+
const toolsList = Object.values(this.tools).map(tool => ({
|
|
319
|
+
name: tool.name,
|
|
320
|
+
description: tool.description,
|
|
321
|
+
inputSchema: tool.inputSchema,
|
|
322
|
+
}));
|
|
323
|
+
return {
|
|
324
|
+
statusCode: 200,
|
|
325
|
+
headers: this.corsHeaders(),
|
|
326
|
+
body: {
|
|
327
|
+
tools: toolsList,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// Execute tool
|
|
332
|
+
if (path === '/tools/execute' && method === 'POST') {
|
|
333
|
+
if (!body || !body.tool || !body.arguments) {
|
|
334
|
+
throw new Error('Request must include "tool" and "arguments" fields');
|
|
335
|
+
}
|
|
336
|
+
const { tool: toolName, arguments: args } = body;
|
|
337
|
+
request.toolName = toolName;
|
|
338
|
+
const tool = this.tools[toolName];
|
|
339
|
+
if (!tool) {
|
|
340
|
+
return {
|
|
341
|
+
statusCode: 404,
|
|
342
|
+
headers: this.corsHeaders(),
|
|
343
|
+
body: {
|
|
344
|
+
error: 'Tool not found',
|
|
345
|
+
message: `Tool "${toolName}" does not exist`,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// Tool allowlist check based on policy
|
|
350
|
+
const token = (0, auth_js_1.extractBearerToken)(request.headers.authorization);
|
|
351
|
+
if (!token) {
|
|
352
|
+
return {
|
|
353
|
+
statusCode: 401,
|
|
354
|
+
headers: this.corsHeaders(),
|
|
355
|
+
body: {
|
|
356
|
+
error: 'Authentication required',
|
|
357
|
+
message: 'Authorization header with UUID is required',
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const policyResult = (0, policy_js_1.lookupClientPolicy)(token, this.config.environment);
|
|
362
|
+
if (!policyResult.policy || !(0, policy_js_1.isToolAllowed)(policyResult.policy, toolName)) {
|
|
363
|
+
return {
|
|
364
|
+
statusCode: 403,
|
|
365
|
+
headers: this.corsHeaders(),
|
|
366
|
+
body: {
|
|
367
|
+
error: 'Forbidden',
|
|
368
|
+
message: 'Tool is not permitted',
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const result = await tool.execute(args);
|
|
374
|
+
return {
|
|
375
|
+
statusCode: 200,
|
|
376
|
+
headers: this.corsHeaders(),
|
|
377
|
+
body: {
|
|
378
|
+
success: true,
|
|
379
|
+
result,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
return {
|
|
385
|
+
statusCode: 400,
|
|
386
|
+
headers: this.corsHeaders(),
|
|
387
|
+
body: {
|
|
388
|
+
error: 'Tool execution failed',
|
|
389
|
+
message: error.message,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// 404 - Not found
|
|
395
|
+
return {
|
|
396
|
+
statusCode: 404,
|
|
397
|
+
headers: this.corsHeaders(),
|
|
398
|
+
body: {
|
|
399
|
+
error: 'Not found',
|
|
400
|
+
message: `Endpoint ${method} ${path} does not exist`,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
corsHeaders() {
|
|
405
|
+
const origins = this.config.allowedOrigins;
|
|
406
|
+
const allowOrigin = origins.includes('*') ? '*' : origins[0];
|
|
407
|
+
return {
|
|
408
|
+
'Content-Type': 'application/json',
|
|
409
|
+
'Access-Control-Allow-Origin': allowOrigin,
|
|
410
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
411
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, MCP-Protocol-Version, MCP-Sequence-Id, MCP-Client-Id, X-Requested-With',
|
|
412
|
+
'Access-Control-Max-Age': '86400',
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
corsResponse() {
|
|
416
|
+
return {
|
|
417
|
+
statusCode: 204,
|
|
418
|
+
headers: this.corsHeaders(),
|
|
419
|
+
body: null,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
rateLimitResponse(limitInfo) {
|
|
423
|
+
return {
|
|
424
|
+
statusCode: 429,
|
|
425
|
+
headers: {
|
|
426
|
+
...this.corsHeaders(),
|
|
427
|
+
'Retry-After': Math.ceil((limitInfo.reset - Date.now()) / 1000).toString(),
|
|
428
|
+
'X-RateLimit-Limit': limitInfo.limit.toString(),
|
|
429
|
+
'X-RateLimit-Remaining': limitInfo.remaining.toString(),
|
|
430
|
+
'X-RateLimit-Reset': limitInfo.reset.toString(),
|
|
431
|
+
},
|
|
432
|
+
body: {
|
|
433
|
+
error: 'Rate limit exceeded',
|
|
434
|
+
message: 'Too many requests. Please try again later.',
|
|
435
|
+
retry_after: Math.ceil((limitInfo.reset - Date.now()) / 1000),
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
errorResponse(message) {
|
|
440
|
+
return {
|
|
441
|
+
statusCode: 500,
|
|
442
|
+
headers: this.corsHeaders(),
|
|
443
|
+
body: {
|
|
444
|
+
error: 'Internal server error',
|
|
445
|
+
message,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
sendResponse(res, response, requestId) {
|
|
450
|
+
if (requestId) {
|
|
451
|
+
res.setHeader('X-Request-Id', requestId);
|
|
452
|
+
}
|
|
453
|
+
res.writeHead(response.statusCode, response.headers);
|
|
454
|
+
res.end(response.body ? JSON.stringify(response.body, null, 2) : '');
|
|
455
|
+
}
|
|
456
|
+
logStructured(entry) {
|
|
457
|
+
console.error(JSON.stringify(entry));
|
|
458
|
+
}
|
|
459
|
+
setupMcpHandlers() {
|
|
460
|
+
// List available tools
|
|
461
|
+
this.mcpServer.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
462
|
+
const toolsList = Object.values(this.tools).map(tool => ({
|
|
463
|
+
name: tool.name,
|
|
464
|
+
description: tool.description,
|
|
465
|
+
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(tool.inputSchema),
|
|
466
|
+
}));
|
|
467
|
+
console.log(`[HTTP][MCP] Listing ${toolsList.length} tools`);
|
|
468
|
+
return {
|
|
469
|
+
tools: toolsList,
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
// Execute tool calls
|
|
473
|
+
this.mcpServer.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
474
|
+
const { name, arguments: args } = request.params;
|
|
475
|
+
console.log(`[HTTP][MCP] Executing tool: ${name}`);
|
|
476
|
+
console.log(`[HTTP][MCP] Arguments:`, JSON.stringify(args, null, 2));
|
|
477
|
+
const tool = this.tools[name];
|
|
478
|
+
if (!tool) {
|
|
479
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const result = await tool.execute(args);
|
|
483
|
+
console.log(`[HTTP][MCP] Tool execution successful`);
|
|
484
|
+
return {
|
|
485
|
+
content: [
|
|
486
|
+
{
|
|
487
|
+
type: 'text',
|
|
488
|
+
text: JSON.stringify(result, null, 2),
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
console.error(`[HTTP][MCP] Tool execution failed:`, error.message);
|
|
495
|
+
throw new Error(`Tool execution failed: ${error.message}`);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async start() {
|
|
500
|
+
await this.mcpServer.connect(this.mcpTransport);
|
|
501
|
+
this.server.listen(this.config.httpPort, this.config.httpHost, () => {
|
|
502
|
+
console.log('[HTTP] ShaBaas Pay MCP Server started');
|
|
503
|
+
console.log(`[HTTP] Listening on http://${this.config.httpHost}:${this.config.httpPort}`);
|
|
504
|
+
console.log(`[HTTP] Environment: ${this.config.environment}`);
|
|
505
|
+
console.log(`[HTTP] Tools available: ${Object.keys(this.tools).length}`);
|
|
506
|
+
console.log('[HTTP] Endpoints:');
|
|
507
|
+
console.log(' - GET /health - Health check');
|
|
508
|
+
console.log(' - GET /tools - List available tools');
|
|
509
|
+
console.log(' - POST /tools/execute - Execute a tool');
|
|
510
|
+
console.log(' - POST/GET /mcp - MCP Streamable HTTP (VS Code / MCP HTTP clients)');
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async stop() {
|
|
514
|
+
await this.mcpTransport.close();
|
|
515
|
+
await new Promise((resolve) => {
|
|
516
|
+
this.server.close(() => {
|
|
517
|
+
console.log('[HTTP] Server stopped');
|
|
518
|
+
resolve();
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
await this.mcpServer.close();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
exports.HttpMcpServer = HttpMcpServer;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Config } from '../config/index.js';
|
|
2
|
+
export declare class StdioMcpServer {
|
|
3
|
+
private server;
|
|
4
|
+
private tools;
|
|
5
|
+
private rateLimiter;
|
|
6
|
+
private rateLimitClientId;
|
|
7
|
+
private config;
|
|
8
|
+
private readTools;
|
|
9
|
+
private writeTools;
|
|
10
|
+
constructor(config: Config);
|
|
11
|
+
private setupHandlers;
|
|
12
|
+
start(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StdioMcpServer = void 0;
|
|
4
|
+
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
5
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
6
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
8
|
+
const client_js_1 = require("../api/client.js");
|
|
9
|
+
const index_js_2 = require("../tools/index.js");
|
|
10
|
+
const rate_limiter_js_1 = require("../security/rate-limiter.js");
|
|
11
|
+
const policy_js_1 = require("../security/policy.js");
|
|
12
|
+
class StdioMcpServer {
|
|
13
|
+
server;
|
|
14
|
+
tools;
|
|
15
|
+
rateLimiter;
|
|
16
|
+
rateLimitClientId = 'stdio-session';
|
|
17
|
+
config;
|
|
18
|
+
readTools = new Set(['get_payment_agreement', 'get_payment_initiation', 'get_auth_token']);
|
|
19
|
+
writeTools = new Set(['create_payment_agreement', 'initiate_payment']);
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
// Initialize API client
|
|
23
|
+
const apiClient = new client_js_1.ShabaasApiClient(config);
|
|
24
|
+
// Create tools
|
|
25
|
+
this.tools = (0, index_js_2.createAllTools)(apiClient, config);
|
|
26
|
+
// Rate limiter (reuse HTTP settings for consistency)
|
|
27
|
+
this.rateLimiter = new rate_limiter_js_1.RateLimiter(config.rateLimitPerMinute, config.rateLimitPerHour);
|
|
28
|
+
// Create MCP server
|
|
29
|
+
this.server = new index_js_1.Server({
|
|
30
|
+
name: 'shabaaspay-mcp',
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
}, {
|
|
33
|
+
capabilities: {
|
|
34
|
+
tools: {},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
this.setupHandlers();
|
|
38
|
+
}
|
|
39
|
+
setupHandlers() {
|
|
40
|
+
const self = this;
|
|
41
|
+
// List available tools
|
|
42
|
+
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
43
|
+
const toolsList = Object.values(self.tools).map(tool => ({
|
|
44
|
+
name: tool.name,
|
|
45
|
+
description: tool.description,
|
|
46
|
+
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(tool.inputSchema),
|
|
47
|
+
}));
|
|
48
|
+
console.error(`[STDIO] Listing ${toolsList.length} tools`);
|
|
49
|
+
return {
|
|
50
|
+
tools: toolsList,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
// Execute tool calls
|
|
54
|
+
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
55
|
+
const { name, arguments: args } = request.params;
|
|
56
|
+
console.error(`[STDIO] Executing tool: ${name}`);
|
|
57
|
+
console.error(`[STDIO] Arguments:`, JSON.stringify(args, null, 2));
|
|
58
|
+
// Auth guard: require authorization field in args (UUID or configured key)
|
|
59
|
+
const authToken = args?.authorization;
|
|
60
|
+
if (!authToken) {
|
|
61
|
+
throw new Error('STDIO authorization failed: missing authorization');
|
|
62
|
+
}
|
|
63
|
+
if (self.config.mcpStdioApiKey && authToken !== self.config.mcpStdioApiKey) {
|
|
64
|
+
throw new Error('STDIO authorization failed: invalid authorization');
|
|
65
|
+
}
|
|
66
|
+
// Tool allowlist via policy
|
|
67
|
+
const policyResult = (0, policy_js_1.lookupClientPolicy)(authToken, self.config.environment);
|
|
68
|
+
if (!policyResult.policy) {
|
|
69
|
+
throw new Error('STDIO authorization failed: access denied');
|
|
70
|
+
}
|
|
71
|
+
if (!(0, policy_js_1.isToolAllowed)(policyResult.policy, name)) {
|
|
72
|
+
throw new Error('STDIO authorization failed: tool not permitted');
|
|
73
|
+
}
|
|
74
|
+
// Rate limiting per stdio session (no client context available)
|
|
75
|
+
const minuteLimit = self.rateLimiter.checkLimit(self.rateLimitClientId, 'minute');
|
|
76
|
+
const hourLimit = self.rateLimiter.checkLimit(self.rateLimitClientId, 'hour');
|
|
77
|
+
if (!minuteLimit.allowed || !hourLimit.allowed) {
|
|
78
|
+
const limitInfo = !minuteLimit.allowed ? minuteLimit : hourLimit;
|
|
79
|
+
throw new Error(`Rate limit exceeded (${limitInfo.limit} requests per ${!minuteLimit.allowed ? 'minute' : 'hour'}). ` +
|
|
80
|
+
`Retry after ${Math.ceil((limitInfo.reset - Date.now()) / 1000)} seconds.`);
|
|
81
|
+
}
|
|
82
|
+
const tool = self.tools[name];
|
|
83
|
+
if (!tool) {
|
|
84
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const result = await tool.execute(args);
|
|
88
|
+
console.error(`[STDIO] Tool execution successful`);
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: JSON.stringify(result, null, 2),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error(`[STDIO] Tool execution failed:`, error.message);
|
|
100
|
+
throw new Error(`Tool execution failed`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async start() {
|
|
105
|
+
console.error('[STDIO] Starting ShaBaas Pay MCP Server...');
|
|
106
|
+
console.error(`[STDIO] Environment: ${process.env.SHABAAS_ENVIRONMENT || 'sandbox'}`);
|
|
107
|
+
console.error(`[STDIO] Tools available: ${Object.keys(this.tools).length}`);
|
|
108
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
109
|
+
await this.server.connect(transport);
|
|
110
|
+
console.error('[STDIO] Server started successfully');
|
|
111
|
+
console.error('[STDIO] Waiting for requests...');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.StdioMcpServer = StdioMcpServer;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_js_1 = require("./config/index.js");
|
|
5
|
+
const http_server_js_1 = require("./server/http-server.js");
|
|
6
|
+
async function main() {
|
|
7
|
+
try {
|
|
8
|
+
// Load configuration
|
|
9
|
+
const config = (0, index_js_1.loadConfig)();
|
|
10
|
+
// Create and start server
|
|
11
|
+
const server = new http_server_js_1.HttpMcpServer(config);
|
|
12
|
+
await server.start();
|
|
13
|
+
// Handle shutdown
|
|
14
|
+
const shutdown = async () => {
|
|
15
|
+
console.log('\n[HTTP] Shutting down gracefully...');
|
|
16
|
+
await server.stop();
|
|
17
|
+
process.exit(0);
|
|
18
|
+
};
|
|
19
|
+
process.on('SIGINT', shutdown);
|
|
20
|
+
process.on('SIGTERM', shutdown);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error('[HTTP] Fatal error:', error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
main();
|