@akropolys/mcp 1.5.3

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/.env ADDED
@@ -0,0 +1,8 @@
1
+ DATABASE_URL=postgresql://neondb_owner:npg_MJIeYWb7O1vT@ep-orange-scene-aqfg2nyj.c-8.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require
2
+ UPSTASH_VECTOR_REST_URL=https://enormous-bream-24617-us1-vector.upstash.io
3
+ UPSTASH_VECTOR_REST_TOKEN=ABgFMGVub3Jtb3VzLWJyZWFtLTI0NjE3LXVzMWFkbWluWXpnMk5HSmhNelV0TVRVNU1TMDBabUptTFdJMFpXRXRZbUUwTXpCaVpEZGlPVGhp
4
+ PORT=8081
5
+ CLERK_JWKS_URL=https://distinct-muskox-86.clerk.accounts.dev/.well-known/jwks.json
6
+ CEREBAS_API_KEY=csk-ft3hjxvdym8e49mxv4ver5yjdjccnfyrnwryd3p3nynt3ynk
7
+ UPSTASH_REDIS_URL=rediss://default:gQAAAAAAAggsAAIgcDIyYTZiYWNiNzMxNmQ0NDM1ODY1NzJiOGZlMzNhN2JlNQ@hopeful-reptile-133164.upstash.io:6379
8
+ NGROK_AUTHTOKEN=3CwKhlWjUvW9IzZGu1ijs2vTGSP_4ffe1CKJhBoohjGq9j8yn
package/dist/index.js ADDED
@@ -0,0 +1,493 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ applyResponseMapping: () => applyResponseMapping,
35
+ cleanParameterSchema: () => cleanParameterSchema,
36
+ getNestedValue: () => getNestedValue,
37
+ isPrivateIp: () => isPrivateIp,
38
+ server: () => server,
39
+ validateUrlForSSRF: () => assertSafeUrl
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+ var import_dotenv = __toESM(require("dotenv"));
43
+ var import_server = require("@modelcontextprotocol/sdk/server/index.js");
44
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
45
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
46
+
47
+ // src/db.ts
48
+ var import_pg = __toESM(require("pg"));
49
+ var { Pool } = import_pg.default;
50
+ var _pool = null;
51
+ function getPool() {
52
+ if (_pool) return _pool;
53
+ const dbUrl = process.env.DATABASE_URL;
54
+ if (!dbUrl) {
55
+ throw new Error("DATABASE_URL environment variable is not defined");
56
+ }
57
+ _pool = new Pool({
58
+ connectionString: dbUrl,
59
+ max: 5,
60
+ idleTimeoutMillis: 3e4,
61
+ connectionTimeoutMillis: 5e3,
62
+ ssl: {
63
+ // Enforce TLS certificate validation.
64
+ // Set PGSSLROOTCERT or NODE_EXTRA_CA_CERTS if using a self-signed cert.
65
+ rejectUnauthorized: true
66
+ }
67
+ });
68
+ _pool.on("error", (err) => {
69
+ console.error("[db] idle client error:", err.message);
70
+ });
71
+ return _pool;
72
+ }
73
+ async function fetchPropertyAndTools(propertyId) {
74
+ const pool = getPool();
75
+ const propRes = await pool.query(
76
+ "SELECT id, site_id, name, api_base, auth_type, auth_token, allow_agent_access FROM developer_properties WHERE id = $1",
77
+ [propertyId]
78
+ );
79
+ if (propRes.rows.length === 0) {
80
+ throw new Error(`Property config with ID "${propertyId}" not found in database`);
81
+ }
82
+ const toolsRes = await pool.query(
83
+ "SELECT id, property_id, name, description, method, path, parameters, response_schema, response_mapping FROM developer_tools WHERE property_id = $1",
84
+ [propertyId]
85
+ );
86
+ return {
87
+ property: propRes.rows[0],
88
+ tools: toolsRes.rows
89
+ };
90
+ }
91
+ async function closePool() {
92
+ if (_pool) {
93
+ await _pool.end();
94
+ _pool = null;
95
+ }
96
+ }
97
+
98
+ // src/cache.ts
99
+ var import_redis = require("redis");
100
+ var redisUrl = process.env.UPSTASH_REDIS_URL;
101
+ var redisClient = redisUrl ? (0, import_redis.createClient)({ url: redisUrl }) : null;
102
+ if (redisClient) {
103
+ redisClient.connect().catch((err) => {
104
+ console.error("Redis connection error:", err);
105
+ });
106
+ }
107
+ async function getCachedConfig(propertyId) {
108
+ if (!redisClient) {
109
+ return null;
110
+ }
111
+ try {
112
+ const cached = await redisClient.get(`mcp:config:${propertyId}`);
113
+ if (cached) {
114
+ return JSON.parse(cached);
115
+ }
116
+ } catch (err) {
117
+ console.error("Redis cache get error:", err);
118
+ }
119
+ return null;
120
+ }
121
+ async function setCachedConfig(propertyId, config) {
122
+ if (!redisClient) {
123
+ return;
124
+ }
125
+ try {
126
+ await redisClient.set(`mcp:config:${propertyId}`, JSON.stringify(config), {
127
+ EX: 3600
128
+ // 1 hour TTL
129
+ });
130
+ } catch (err) {
131
+ console.error("Redis cache set error:", err);
132
+ }
133
+ }
134
+
135
+ // src/vault.ts
136
+ var import_crypto = __toESM(require("crypto"));
137
+ var LocalAESVault = class {
138
+ masterKey;
139
+ constructor(masterKeyEnvVar = "AKROPOLYS_KMS_MASTER_KEY") {
140
+ const keyStr = process.env[masterKeyEnvVar];
141
+ if (!keyStr) {
142
+ throw new Error(`Master key environment variable ${masterKeyEnvVar} is not defined`);
143
+ }
144
+ this.masterKey = import_crypto.default.createHash("sha256").update(keyStr).digest();
145
+ }
146
+ async encrypt(plaintext) {
147
+ const iv = import_crypto.default.randomBytes(12);
148
+ const cipher = import_crypto.default.createCipheriv("aes-256-gcm", this.masterKey, iv);
149
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
150
+ encrypted += cipher.final("hex");
151
+ const authTag = cipher.getAuthTag().toString("hex");
152
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
153
+ }
154
+ async decrypt(ciphertext) {
155
+ const parts = ciphertext.split(":");
156
+ if (parts.length !== 3) {
157
+ throw new Error("Invalid ciphertext format");
158
+ }
159
+ const iv = Buffer.from(parts[0], "hex");
160
+ const authTag = Buffer.from(parts[1], "hex");
161
+ const encryptedData = parts[2];
162
+ const decipher = import_crypto.default.createDecipheriv("aes-256-gcm", this.masterKey, iv);
163
+ decipher.setAuthTag(authTag);
164
+ let decrypted = decipher.update(encryptedData, "hex", "utf8");
165
+ decrypted += decipher.final("utf8");
166
+ return decrypted;
167
+ }
168
+ };
169
+
170
+ // src/ssrfValidator.ts
171
+ var import_dns = __toESM(require("dns"));
172
+ var import_util = require("util");
173
+ var lookup = (0, import_util.promisify)(import_dns.default.lookup);
174
+ function isPrivateIp(ip) {
175
+ const ipv4MappedMatch = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
176
+ const normalizedIp = ipv4MappedMatch ? ipv4MappedMatch[1] : ip;
177
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(normalizedIp)) {
178
+ const parts = normalizedIp.split(".").map(Number);
179
+ if (parts.some(isNaN) || parts.length !== 4) return true;
180
+ if (parts[0] === 127) return true;
181
+ if (parts[0] === 10) return true;
182
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
183
+ if (parts[0] === 192 && parts[1] === 168) return true;
184
+ if (parts[0] === 169 && parts[1] === 254) return true;
185
+ if (parts[0] === 0) return true;
186
+ return false;
187
+ }
188
+ if (normalizedIp === "::1" || normalizedIp === "::") return true;
189
+ if (/^fe[89ab]/i.test(normalizedIp)) return true;
190
+ if (/^f[cd]/i.test(normalizedIp)) return true;
191
+ return false;
192
+ }
193
+ async function assertSafeUrl(urlStr) {
194
+ const url = new URL(urlStr);
195
+ const hostname = url.hostname;
196
+ const res = await lookup(hostname);
197
+ if (isPrivateIp(res.address)) {
198
+ throw new Error(`SSRF Prevention: outbound requests to private IP address ${res.address} (resolved from "${hostname}") are prohibited`);
199
+ }
200
+ const safeUrl = new URL(urlStr);
201
+ safeUrl.hostname = res.address;
202
+ return {
203
+ safeUrl: safeUrl.toString(),
204
+ hostHeader: hostname
205
+ // caller must forward this as the Host header
206
+ };
207
+ }
208
+
209
+ // src/rateLimiter.ts
210
+ var McpRateLimiter = class {
211
+ client;
212
+ limit;
213
+ windowSeconds;
214
+ constructor(redisClient2, limit = 100, windowSeconds = 60) {
215
+ this.client = redisClient2;
216
+ this.limit = limit;
217
+ this.windowSeconds = windowSeconds;
218
+ }
219
+ async assertAllowed(propertyId) {
220
+ if (!this.client) {
221
+ throw new Error("Rate limiting is not configured (Redis unavailable). Request denied.");
222
+ }
223
+ const key = `mcp:ratelimit:${propertyId}`;
224
+ const current = await this.client.incr(key);
225
+ if (current === 1) {
226
+ await this.client.expire(key, this.windowSeconds);
227
+ }
228
+ if (current > this.limit) {
229
+ throw new Error(
230
+ `Rate limit exceeded: property "${propertyId}" has exceeded the limit of ${this.limit} requests per ${this.windowSeconds} seconds.`
231
+ );
232
+ }
233
+ }
234
+ };
235
+
236
+ // src/index.ts
237
+ var rateLimiter = new McpRateLimiter(redisClient);
238
+ import_dotenv.default.config();
239
+ var vault = null;
240
+ try {
241
+ vault = new LocalAESVault();
242
+ } catch (err) {
243
+ console.warn(`\u26A0\uFE0F WARNING: KMS Vault failed to initialize: ${err.message}. Raw tokens will be used.`);
244
+ }
245
+ async function getConfig() {
246
+ const propertyId = process.env.AKROPOLYS_PROPERTY_ID;
247
+ if (!propertyId) {
248
+ throw new Error("AKROPOLYS_PROPERTY_ID environment variable is required");
249
+ }
250
+ const cached = await getCachedConfig(propertyId);
251
+ if (cached) {
252
+ return cached;
253
+ }
254
+ const config = await fetchPropertyAndTools(propertyId);
255
+ await setCachedConfig(propertyId, config);
256
+ return config;
257
+ }
258
+ async function getDecryptedToken(token) {
259
+ if (!token) return null;
260
+ if (!vault) return token;
261
+ try {
262
+ return await vault.decrypt(token);
263
+ } catch (err) {
264
+ return token;
265
+ }
266
+ }
267
+ function getNestedValue(obj, path) {
268
+ if (!obj) return void 0;
269
+ const parts = path.split(".");
270
+ let current = obj;
271
+ for (const part of parts) {
272
+ if (current === null || current === void 0) return void 0;
273
+ const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
274
+ if (arrayMatch) {
275
+ const key = arrayMatch[1];
276
+ const index = parseInt(arrayMatch[2], 10);
277
+ current = current[key];
278
+ if (Array.isArray(current)) {
279
+ current = current[index];
280
+ } else {
281
+ return void 0;
282
+ }
283
+ } else {
284
+ if (Array.isArray(current)) {
285
+ current = current.map((item) => item ? item[part] : void 0);
286
+ } else {
287
+ current = current[part];
288
+ }
289
+ }
290
+ }
291
+ return current;
292
+ }
293
+ function applyResponseMapping(payload, mapping) {
294
+ if (!payload) return payload;
295
+ if (Array.isArray(payload)) {
296
+ return payload.map((item) => applyResponseMapping(item, mapping));
297
+ }
298
+ const mappedResult = {};
299
+ let hasMappedAny = false;
300
+ for (const [targetKey, sourcePath] of Object.entries(mapping)) {
301
+ if (typeof sourcePath === "string") {
302
+ const val = getNestedValue(payload, sourcePath);
303
+ if (val !== void 0) {
304
+ mappedResult[targetKey] = val;
305
+ hasMappedAny = true;
306
+ }
307
+ }
308
+ }
309
+ return hasMappedAny ? mappedResult : payload;
310
+ }
311
+ function cleanParameterSchema(params) {
312
+ if (!params || typeof params !== "object") {
313
+ return { type: "object", properties: {} };
314
+ }
315
+ const cleaned = JSON.parse(JSON.stringify(params));
316
+ if (cleaned.properties && typeof cleaned.properties === "object") {
317
+ for (const key of Object.keys(cleaned.properties)) {
318
+ const prop = cleaned.properties[key];
319
+ if (prop && typeof prop === "object") {
320
+ delete prop.location;
321
+ }
322
+ }
323
+ }
324
+ return cleaned;
325
+ }
326
+ var server = new import_server.Server(
327
+ {
328
+ name: "akropolys-mcp",
329
+ version: "1.2.3"
330
+ },
331
+ {
332
+ capabilities: {
333
+ tools: {}
334
+ }
335
+ }
336
+ );
337
+ server.setRequestHandler(import_types.ListToolsRequestSchema, async () => {
338
+ try {
339
+ const { tools } = await getConfig();
340
+ return {
341
+ tools: tools.map((t) => ({
342
+ name: t.name,
343
+ description: t.description,
344
+ inputSchema: cleanParameterSchema(t.parameters)
345
+ }))
346
+ };
347
+ } catch (err) {
348
+ console.error("Error listing tools:", err);
349
+ return {
350
+ tools: []
351
+ };
352
+ }
353
+ });
354
+ server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
355
+ const { name, arguments: args } = request.params;
356
+ const argumentsObj = args || {};
357
+ try {
358
+ const { property, tools } = await getConfig();
359
+ if (!property.allow_agent_access) {
360
+ throw new Error(`Agent access is not enabled for property "${property.id}"`);
361
+ }
362
+ const tool = tools.find((t) => t.name === name);
363
+ if (!tool) {
364
+ throw new Error(`Tool "${name}" not found`);
365
+ }
366
+ const decryptedAuthToken = await getDecryptedToken(property.auth_token);
367
+ let resolvedPath = tool.path;
368
+ const queryParams = {};
369
+ const headers = {};
370
+ const bodyParams = {};
371
+ if (property.auth_type === "bearer" && decryptedAuthToken) {
372
+ headers["Authorization"] = `Bearer ${decryptedAuthToken}`;
373
+ } else if (property.auth_type === "api_key" && decryptedAuthToken) {
374
+ headers["X-API-Key"] = decryptedAuthToken;
375
+ headers["X-Akropolys-Token"] = decryptedAuthToken;
376
+ headers["Authorization"] = decryptedAuthToken;
377
+ }
378
+ const paramDefs = tool.parameters?.properties || {};
379
+ const requiredParams = tool.parameters?.required || [];
380
+ for (const reqField of requiredParams) {
381
+ if (argumentsObj[reqField] === void 0) {
382
+ throw new Error(`Missing required parameter: "${reqField}"`);
383
+ }
384
+ }
385
+ for (const [key, val] of Object.entries(argumentsObj)) {
386
+ const def = paramDefs[key] || {};
387
+ const location = def.location || "body";
388
+ if (location === "path") {
389
+ resolvedPath = resolvedPath.replace(new RegExp(`:${key}`, "g"), String(val)).replace(new RegExp(`{${key}}`, "g"), String(val));
390
+ } else if (location === "query") {
391
+ queryParams[key] = String(val);
392
+ } else if (location === "header") {
393
+ headers[key] = String(val);
394
+ } else if (location === "body") {
395
+ bodyParams[key] = val;
396
+ }
397
+ }
398
+ const baseUrl = property.api_base.replace(/\/+$/, "");
399
+ const urlPath = resolvedPath.replace(/^\/+/, "");
400
+ let fullUrl = `${baseUrl}/${urlPath}`;
401
+ if (Object.keys(queryParams).length > 0) {
402
+ const qs = new URLSearchParams(queryParams).toString();
403
+ fullUrl += `?${qs}`;
404
+ }
405
+ const requestOptions = {
406
+ method: tool.method.toUpperCase(),
407
+ headers
408
+ };
409
+ if (["POST", "PUT", "PATCH"].includes(tool.method.toUpperCase())) {
410
+ if (!headers["Content-Type"]) {
411
+ headers["Content-Type"] = "application/json";
412
+ }
413
+ requestOptions.body = JSON.stringify(bodyParams);
414
+ }
415
+ await rateLimiter.assertAllowed(property.id);
416
+ const { safeUrl, hostHeader } = await assertSafeUrl(fullUrl);
417
+ headers["Host"] = hostHeader;
418
+ const response = await fetch(safeUrl, requestOptions);
419
+ const text = await response.text();
420
+ let jsonPayload;
421
+ try {
422
+ jsonPayload = JSON.parse(text);
423
+ } catch {
424
+ jsonPayload = { responseText: text };
425
+ }
426
+ if (!response.ok) {
427
+ return {
428
+ content: [
429
+ {
430
+ type: "text",
431
+ text: `API request failed with status ${response.status}: ${JSON.stringify(jsonPayload)}`
432
+ }
433
+ ],
434
+ isError: true
435
+ };
436
+ }
437
+ let normalized = jsonPayload;
438
+ if (tool.response_mapping && typeof tool.response_mapping === "object" && Object.keys(tool.response_mapping).length > 0) {
439
+ normalized = applyResponseMapping(jsonPayload, tool.response_mapping);
440
+ }
441
+ return {
442
+ content: [
443
+ {
444
+ type: "text",
445
+ text: JSON.stringify(normalized, null, 2)
446
+ }
447
+ ]
448
+ };
449
+ } catch (err) {
450
+ console.error(`Error executing tool "${name}":`, err);
451
+ return {
452
+ content: [
453
+ {
454
+ type: "text",
455
+ text: `Error: ${err.message}`
456
+ }
457
+ ],
458
+ isError: true
459
+ };
460
+ }
461
+ });
462
+ async function main() {
463
+ const propertyId = process.env.AKROPOLYS_PROPERTY_ID;
464
+ if (!propertyId) {
465
+ console.error("\u274C ERROR: AKROPOLYS_PROPERTY_ID environment variable is required");
466
+ process.exit(1);
467
+ }
468
+ const transport = new import_stdio.StdioServerTransport();
469
+ await server.connect(transport);
470
+ console.error(`\u2713 Akropolys MCP Proxy Server running for property: ${propertyId}`);
471
+ }
472
+ if (process.env.NODE_ENV !== "test") {
473
+ const shutdown = async (signal) => {
474
+ console.error(`[mcp] ${signal} received \u2014 shutting down`);
475
+ await closePool();
476
+ process.exit(0);
477
+ };
478
+ process.on("SIGINT", () => shutdown("SIGINT"));
479
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
480
+ main().catch((err) => {
481
+ console.error("Fatal error in MCP server:", err);
482
+ process.exit(1);
483
+ });
484
+ }
485
+ // Annotate the CommonJS export names for ESM import in node:
486
+ 0 && (module.exports = {
487
+ applyResponseMapping,
488
+ cleanParameterSchema,
489
+ getNestedValue,
490
+ isPrivateIp,
491
+ server,
492
+ validateUrlForSSRF
493
+ });