@gxp-dev/tools 2.0.10 → 2.0.12

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.
@@ -0,0 +1,524 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GxP API Specs MCP Server
5
+ *
6
+ * Provides API documentation (OpenAPI, AsyncAPI, Webhooks) to AI coding assistants
7
+ * via the Model Context Protocol (MCP).
8
+ *
9
+ * Features:
10
+ * - Fetches specs based on VITE_API_ENV environment variable
11
+ * - Caches specs in memory for performance
12
+ * - Provides tools for fetching full specs or searching endpoints
13
+ *
14
+ * Usage:
15
+ * node mcp/gxp-api-server.js
16
+ *
17
+ * Configure in your AI tool's MCP settings to enable API-aware assistance.
18
+ */
19
+
20
+ const readline = require("readline");
21
+ const fs = require("fs");
22
+ const path = require("path");
23
+
24
+ // Environment URL configuration (matches constants.js)
25
+ const ENVIRONMENT_URLS = {
26
+ production: {
27
+ apiBaseUrl: "https://api.gramercy.cloud",
28
+ openApiSpec: "https://api.gramercy.cloud/api-specs/openapi.json",
29
+ asyncApiSpec: "https://api.gramercy.cloud/api-specs/asyncapi.json",
30
+ webhookSpec: "https://api.gramercy.cloud/api-specs/webhooks.json",
31
+ },
32
+ staging: {
33
+ apiBaseUrl: "https://api.efz-staging.env.eventfinity.app",
34
+ openApiSpec:
35
+ "https://api.efz-staging.env.eventfinity.app/api-specs/openapi.json",
36
+ asyncApiSpec:
37
+ "https://api.efz-staging.env.eventfinity.app/api-specs/asyncapi.json",
38
+ webhookSpec:
39
+ "https://api.efz-staging.env.eventfinity.app/api-specs/webhooks.json",
40
+ },
41
+ testing: {
42
+ apiBaseUrl: "https://api.zenith-develop-testing.env.eventfinity.app",
43
+ openApiSpec:
44
+ "https://api.zenith-develop-testing.env.eventfinity.app/api-specs/openapi.json",
45
+ asyncApiSpec:
46
+ "https://api.zenith-develop-testing.env.eventfinity.app/api-specs/asyncapi.json",
47
+ webhookSpec:
48
+ "https://api.zenith-develop-testing.env.eventfinity.app/api-specs/webhooks.json",
49
+ },
50
+ develop: {
51
+ apiBaseUrl: "https://api.zenith-develop.env.eventfinity.app",
52
+ openApiSpec:
53
+ "https://api.zenith-develop.env.eventfinity.app/api-specs/openapi.json",
54
+ asyncApiSpec:
55
+ "https://api.zenith-develop.env.eventfinity.app/api-specs/asyncapi.json",
56
+ webhookSpec:
57
+ "https://api.zenith-develop.env.eventfinity.app/api-specs/webhooks.json",
58
+ },
59
+ local: {
60
+ apiBaseUrl: "https://dashboard.eventfinity.test",
61
+ openApiSpec: "https://api.eventfinity.test/api-specs/openapi.json",
62
+ asyncApiSpec: "https://api.eventfinity.test/api-specs/asyncapi.json",
63
+ webhookSpec: "https://api.eventfinity.test/api-specs/webhooks.json",
64
+ },
65
+ };
66
+
67
+ // Cache for fetched specs
68
+ const specCache = {
69
+ openapi: null,
70
+ asyncapi: null,
71
+ webhooks: null,
72
+ lastFetch: null,
73
+ };
74
+
75
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
76
+
77
+ /**
78
+ * Get current environment from .env file or default
79
+ */
80
+ function getEnvironment() {
81
+ // Try to read from .env file in current directory
82
+ const envPath = path.join(process.cwd(), ".env");
83
+ if (fs.existsSync(envPath)) {
84
+ const envContent = fs.readFileSync(envPath, "utf-8");
85
+ const match = envContent.match(/VITE_API_ENV=(\w+)/);
86
+ if (match) {
87
+ return match[1];
88
+ }
89
+ }
90
+
91
+ // Fall back to environment variable or default
92
+ return process.env.VITE_API_ENV || process.env.API_ENV || "develop";
93
+ }
94
+
95
+ /**
96
+ * Get URLs for current environment
97
+ */
98
+ function getEnvUrls() {
99
+ const env = getEnvironment();
100
+ return ENVIRONMENT_URLS[env] || ENVIRONMENT_URLS.develop;
101
+ }
102
+
103
+ /**
104
+ * Fetch a spec with caching
105
+ */
106
+ async function fetchSpec(specType) {
107
+ const urls = getEnvUrls();
108
+ const urlMap = {
109
+ openapi: urls.openApiSpec,
110
+ asyncapi: urls.asyncApiSpec,
111
+ webhooks: urls.webhookSpec,
112
+ };
113
+
114
+ const url = urlMap[specType];
115
+ if (!url) {
116
+ throw new Error(`Unknown spec type: ${specType}`);
117
+ }
118
+
119
+ // Check cache
120
+ const now = Date.now();
121
+ if (
122
+ specCache[specType] &&
123
+ specCache.lastFetch &&
124
+ now - specCache.lastFetch < CACHE_TTL
125
+ ) {
126
+ return specCache[specType];
127
+ }
128
+
129
+ // Fetch fresh
130
+ try {
131
+ const response = await fetch(url);
132
+ if (!response.ok) {
133
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
134
+ }
135
+ const data = await response.json();
136
+ specCache[specType] = data;
137
+ specCache.lastFetch = now;
138
+ return data;
139
+ } catch (error) {
140
+ throw new Error(`Failed to fetch ${specType} spec from ${url}: ${error.message}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Search OpenAPI spec for endpoints matching a query
146
+ */
147
+ function searchEndpoints(spec, query) {
148
+ const results = [];
149
+ const queryLower = query.toLowerCase();
150
+
151
+ if (spec.paths) {
152
+ for (const [path, methods] of Object.entries(spec.paths)) {
153
+ for (const [method, details] of Object.entries(methods)) {
154
+ if (
155
+ typeof details === "object" &&
156
+ (path.toLowerCase().includes(queryLower) ||
157
+ details.summary?.toLowerCase().includes(queryLower) ||
158
+ details.description?.toLowerCase().includes(queryLower) ||
159
+ details.operationId?.toLowerCase().includes(queryLower) ||
160
+ details.tags?.some((t) => t.toLowerCase().includes(queryLower)))
161
+ ) {
162
+ results.push({
163
+ path,
164
+ method: method.toUpperCase(),
165
+ summary: details.summary || "",
166
+ description: details.description || "",
167
+ operationId: details.operationId || "",
168
+ tags: details.tags || [],
169
+ parameters: details.parameters || [],
170
+ requestBody: details.requestBody || null,
171
+ responses: Object.keys(details.responses || {}),
172
+ });
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ return results;
179
+ }
180
+
181
+ /**
182
+ * Search AsyncAPI spec for channels/events matching a query
183
+ */
184
+ function searchEvents(spec, query) {
185
+ const results = [];
186
+ const queryLower = query.toLowerCase();
187
+
188
+ if (spec.channels) {
189
+ for (const [channel, details] of Object.entries(spec.channels)) {
190
+ if (
191
+ channel.toLowerCase().includes(queryLower) ||
192
+ details.description?.toLowerCase().includes(queryLower)
193
+ ) {
194
+ const operations = [];
195
+ if (details.publish) {
196
+ operations.push({
197
+ type: "publish",
198
+ summary: details.publish.summary || "",
199
+ message: details.publish.message || null,
200
+ });
201
+ }
202
+ if (details.subscribe) {
203
+ operations.push({
204
+ type: "subscribe",
205
+ summary: details.subscribe.summary || "",
206
+ message: details.subscribe.message || null,
207
+ });
208
+ }
209
+
210
+ results.push({
211
+ channel,
212
+ description: details.description || "",
213
+ operations,
214
+ });
215
+ }
216
+ }
217
+ }
218
+
219
+ return results;
220
+ }
221
+
222
+ /**
223
+ * Get endpoint details by path and method
224
+ */
225
+ function getEndpointDetails(spec, path, method) {
226
+ const methodLower = method.toLowerCase();
227
+ const endpoint = spec.paths?.[path]?.[methodLower];
228
+
229
+ if (!endpoint) {
230
+ return null;
231
+ }
232
+
233
+ return {
234
+ path,
235
+ method: method.toUpperCase(),
236
+ summary: endpoint.summary || "",
237
+ description: endpoint.description || "",
238
+ operationId: endpoint.operationId || "",
239
+ tags: endpoint.tags || [],
240
+ parameters: endpoint.parameters || [],
241
+ requestBody: endpoint.requestBody || null,
242
+ responses: endpoint.responses || {},
243
+ security: endpoint.security || spec.security || [],
244
+ };
245
+ }
246
+
247
+ // MCP Server Implementation
248
+ const SERVER_INFO = {
249
+ name: "gxp-api-server",
250
+ version: "1.0.0",
251
+ description: "GxP API documentation server for AI coding assistants",
252
+ };
253
+
254
+ const TOOLS = [
255
+ {
256
+ name: "get_openapi_spec",
257
+ description:
258
+ "Fetch the full OpenAPI specification for the GxP API. Returns the complete spec including all endpoints, schemas, and documentation.",
259
+ inputSchema: {
260
+ type: "object",
261
+ properties: {},
262
+ required: [],
263
+ },
264
+ },
265
+ {
266
+ name: "get_asyncapi_spec",
267
+ description:
268
+ "Fetch the AsyncAPI specification for GxP WebSocket events. Returns channel definitions, message schemas, and event documentation.",
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {},
272
+ required: [],
273
+ },
274
+ },
275
+ {
276
+ name: "search_api_endpoints",
277
+ description:
278
+ "Search for API endpoints matching a query. Searches path, summary, description, operation ID, and tags.",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ query: {
283
+ type: "string",
284
+ description:
285
+ "Search term to find matching endpoints (e.g., 'attendee', 'check-in', 'event')",
286
+ },
287
+ },
288
+ required: ["query"],
289
+ },
290
+ },
291
+ {
292
+ name: "search_websocket_events",
293
+ description:
294
+ "Search for WebSocket channels/events matching a query. Searches channel names and descriptions.",
295
+ inputSchema: {
296
+ type: "object",
297
+ properties: {
298
+ query: {
299
+ type: "string",
300
+ description:
301
+ "Search term to find matching events (e.g., 'message', 'created', 'updated')",
302
+ },
303
+ },
304
+ required: ["query"],
305
+ },
306
+ },
307
+ {
308
+ name: "get_endpoint_details",
309
+ description:
310
+ "Get detailed information about a specific API endpoint including parameters, request body, and responses.",
311
+ inputSchema: {
312
+ type: "object",
313
+ properties: {
314
+ path: {
315
+ type: "string",
316
+ description: "API endpoint path (e.g., '/api/v1/attendees')",
317
+ },
318
+ method: {
319
+ type: "string",
320
+ description: "HTTP method (GET, POST, PUT, PATCH, DELETE)",
321
+ },
322
+ },
323
+ required: ["path", "method"],
324
+ },
325
+ },
326
+ {
327
+ name: "get_api_environment",
328
+ description:
329
+ "Get the current API environment configuration including base URL and spec URLs.",
330
+ inputSchema: {
331
+ type: "object",
332
+ properties: {},
333
+ required: [],
334
+ },
335
+ },
336
+ ];
337
+
338
+ /**
339
+ * Handle MCP tool calls
340
+ */
341
+ async function handleToolCall(name, args) {
342
+ switch (name) {
343
+ case "get_openapi_spec": {
344
+ const spec = await fetchSpec("openapi");
345
+ return {
346
+ content: [
347
+ {
348
+ type: "text",
349
+ text: JSON.stringify(spec, null, 2),
350
+ },
351
+ ],
352
+ };
353
+ }
354
+
355
+ case "get_asyncapi_spec": {
356
+ const spec = await fetchSpec("asyncapi");
357
+ return {
358
+ content: [
359
+ {
360
+ type: "text",
361
+ text: JSON.stringify(spec, null, 2),
362
+ },
363
+ ],
364
+ };
365
+ }
366
+
367
+ case "search_api_endpoints": {
368
+ const spec = await fetchSpec("openapi");
369
+ const results = searchEndpoints(spec, args.query);
370
+ return {
371
+ content: [
372
+ {
373
+ type: "text",
374
+ text:
375
+ results.length > 0
376
+ ? JSON.stringify(results, null, 2)
377
+ : `No endpoints found matching "${args.query}"`,
378
+ },
379
+ ],
380
+ };
381
+ }
382
+
383
+ case "search_websocket_events": {
384
+ const spec = await fetchSpec("asyncapi");
385
+ const results = searchEvents(spec, args.query);
386
+ return {
387
+ content: [
388
+ {
389
+ type: "text",
390
+ text:
391
+ results.length > 0
392
+ ? JSON.stringify(results, null, 2)
393
+ : `No events found matching "${args.query}"`,
394
+ },
395
+ ],
396
+ };
397
+ }
398
+
399
+ case "get_endpoint_details": {
400
+ const spec = await fetchSpec("openapi");
401
+ const details = getEndpointDetails(spec, args.path, args.method);
402
+ return {
403
+ content: [
404
+ {
405
+ type: "text",
406
+ text: details
407
+ ? JSON.stringify(details, null, 2)
408
+ : `Endpoint not found: ${args.method} ${args.path}`,
409
+ },
410
+ ],
411
+ };
412
+ }
413
+
414
+ case "get_api_environment": {
415
+ const env = getEnvironment();
416
+ const urls = getEnvUrls();
417
+ return {
418
+ content: [
419
+ {
420
+ type: "text",
421
+ text: JSON.stringify(
422
+ {
423
+ environment: env,
424
+ ...urls,
425
+ },
426
+ null,
427
+ 2
428
+ ),
429
+ },
430
+ ],
431
+ };
432
+ }
433
+
434
+ default:
435
+ throw new Error(`Unknown tool: ${name}`);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Process MCP JSON-RPC request
441
+ */
442
+ async function processRequest(request) {
443
+ const { method, params, id } = request;
444
+
445
+ try {
446
+ let result;
447
+
448
+ switch (method) {
449
+ case "initialize":
450
+ result = {
451
+ protocolVersion: "2024-11-05",
452
+ capabilities: {
453
+ tools: {},
454
+ },
455
+ serverInfo: SERVER_INFO,
456
+ };
457
+ break;
458
+
459
+ case "tools/list":
460
+ result = { tools: TOOLS };
461
+ break;
462
+
463
+ case "tools/call":
464
+ result = await handleToolCall(params.name, params.arguments || {});
465
+ break;
466
+
467
+ case "notifications/initialized":
468
+ // No response needed for notifications
469
+ return null;
470
+
471
+ default:
472
+ throw new Error(`Unknown method: ${method}`);
473
+ }
474
+
475
+ return { jsonrpc: "2.0", id, result };
476
+ } catch (error) {
477
+ return {
478
+ jsonrpc: "2.0",
479
+ id,
480
+ error: {
481
+ code: -32603,
482
+ message: error.message,
483
+ },
484
+ };
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Main server loop
490
+ */
491
+ async function main() {
492
+ const rl = readline.createInterface({
493
+ input: process.stdin,
494
+ output: process.stdout,
495
+ terminal: false,
496
+ });
497
+
498
+ for await (const line of rl) {
499
+ if (!line.trim()) continue;
500
+
501
+ try {
502
+ const request = JSON.parse(line);
503
+ const response = await processRequest(request);
504
+
505
+ if (response) {
506
+ console.log(JSON.stringify(response));
507
+ }
508
+ } catch (error) {
509
+ console.log(
510
+ JSON.stringify({
511
+ jsonrpc: "2.0",
512
+ id: null,
513
+ error: {
514
+ code: -32700,
515
+ message: `Parse error: ${error.message}`,
516
+ },
517
+ })
518
+ );
519
+ }
520
+ }
521
+ }
522
+
523
+ // Run server
524
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gxp-dev/tools",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "description": "Dev tools to create platform plugins",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "dist/tui",
12
+ "mcp",
12
13
  "README.md",
13
14
  "runtime",
14
15
  "template",
@@ -34,7 +35,8 @@
34
35
  "url": "git+ssh://git@github.com/GramercyTech/gx-devtools.git"
35
36
  },
36
37
  "bin": {
37
- "gxdev": "bin/gx-devtools.js"
38
+ "gxdev": "bin/gx-devtools.js",
39
+ "gxp-api-server": "mcp/gxp-api-server.js"
38
40
  },
39
41
  "keywords": [
40
42
  "tunnel",
@@ -375,6 +375,14 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
375
375
  throw new Error(`DELETE ${endpoint}: ${error.message}`);
376
376
  }
377
377
  }
378
+ async function callApi(endpoint, method, data = {}) {
379
+ try {
380
+ const response = await apiClient[method](endpoint, data);
381
+ return response.data;
382
+ } catch (error) {
383
+ throw new Error(`${method} ${endpoint}: ${error.message}`);
384
+ }
385
+ }
378
386
 
379
387
  // Utility methods
380
388
  function getString(key, fallback = "") {
@@ -523,6 +531,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
523
531
  apiPatch,
524
532
  apiPut,
525
533
  apiDelete,
534
+ callApi,
526
535
 
527
536
  // Utility methods
528
537
  getString,