@gxp-dev/tools 2.0.81 → 2.0.83

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,192 @@
1
+ /**
2
+ * MCP tools for OpenAPI data-model introspection.
3
+ *
4
+ * - describe_data_models : enumerate or detail OpenAPI components.schemas.
5
+ * With no args, returns every schema name + description + required +
6
+ * property summary. Pass `name` for a single exact-match model, or
7
+ * `query` for a substring filter on name + description.
8
+ *
9
+ * Schema property summaries resolve $ref one level deep (showing the
10
+ * referenced model name as the type) and walk allOf compositions so the
11
+ * caller sees inherited fields in one place.
12
+ */
13
+
14
+ const { fetchSpec } = require("./specs")
15
+
16
+ function contentResult(obj) {
17
+ return {
18
+ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
19
+ }
20
+ }
21
+
22
+ function resolveRef(spec, ref) {
23
+ if (!ref || typeof ref !== "string" || !ref.startsWith("#/")) return null
24
+ const segs = ref.slice(2).split("/")
25
+ let cur = spec
26
+ for (const s of segs) {
27
+ if (!cur || typeof cur !== "object") return null
28
+ cur = cur[s]
29
+ }
30
+ return cur || null
31
+ }
32
+
33
+ function refName(ref) {
34
+ if (!ref || typeof ref !== "string") return null
35
+ return ref.split("/").pop()
36
+ }
37
+
38
+ function summarizeProperty(prop, spec) {
39
+ if (!prop || typeof prop !== "object") {
40
+ return { type: "unknown" }
41
+ }
42
+ if (prop.$ref) {
43
+ return {
44
+ type: refName(prop.$ref) || "ref",
45
+ description: prop.description || undefined,
46
+ }
47
+ }
48
+ const out = {
49
+ type:
50
+ prop.type ||
51
+ (prop.oneOf
52
+ ? "oneOf"
53
+ : prop.anyOf
54
+ ? "anyOf"
55
+ : prop.allOf
56
+ ? "allOf"
57
+ : "unknown"),
58
+ description: prop.description || undefined,
59
+ format: prop.format || undefined,
60
+ enum: prop.enum || undefined,
61
+ nullable: prop.nullable || undefined,
62
+ }
63
+ if (prop.type === "array" && prop.items) {
64
+ out.items = prop.items.$ref
65
+ ? refName(prop.items.$ref)
66
+ : prop.items.type || "unknown"
67
+ }
68
+ for (const k of Object.keys(out)) {
69
+ if (out[k] === undefined) delete out[k]
70
+ }
71
+ return out
72
+ }
73
+
74
+ function summarizeProperties(schema, spec, seen = new Set()) {
75
+ if (!schema || typeof schema !== "object") return null
76
+ let target = schema
77
+ if (target.$ref) {
78
+ if (seen.has(target.$ref)) return null
79
+ seen.add(target.$ref)
80
+ target = resolveRef(spec, target.$ref) || target
81
+ }
82
+ const out = {}
83
+ if (Array.isArray(target.allOf)) {
84
+ for (const piece of target.allOf) {
85
+ const sub = summarizeProperties(piece, spec, seen)
86
+ if (sub) Object.assign(out, sub)
87
+ }
88
+ }
89
+ if (target.properties && typeof target.properties === "object") {
90
+ for (const [name, prop] of Object.entries(target.properties)) {
91
+ out[name] = summarizeProperty(prop, spec)
92
+ }
93
+ }
94
+ return Object.keys(out).length > 0 ? out : null
95
+ }
96
+
97
+ async function describeDataModels({ name, query } = {}) {
98
+ const spec = await fetchSpec("openapi")
99
+ const schemas = (spec && spec.components && spec.components.schemas) || {}
100
+ const entries = Object.entries(schemas)
101
+
102
+ if (name) {
103
+ const hit = entries.find(([n]) => n === name)
104
+ if (!hit) {
105
+ return {
106
+ ok: false,
107
+ error: `Model not found: ${name}`,
108
+ available_sample: entries.map(([n]) => n).slice(0, 50),
109
+ available_count: entries.length,
110
+ }
111
+ }
112
+ const [hitName, hitSchema] = hit
113
+ return {
114
+ ok: true,
115
+ count: 1,
116
+ models: [
117
+ {
118
+ name: hitName,
119
+ description: hitSchema.description || "",
120
+ type: hitSchema.type || (hitSchema.$ref ? "ref" : "object"),
121
+ required: hitSchema.required || [],
122
+ properties: summarizeProperties(hitSchema, spec) || {},
123
+ },
124
+ ],
125
+ }
126
+ }
127
+
128
+ let filtered = entries
129
+ if (query) {
130
+ const q = String(query).toLowerCase()
131
+ filtered = entries.filter(([n, s]) => {
132
+ if (n.toLowerCase().includes(q)) return true
133
+ const desc = (s && s.description) || ""
134
+ return desc.toLowerCase().includes(q)
135
+ })
136
+ }
137
+
138
+ const models = filtered.map(([n, s]) => ({
139
+ name: n,
140
+ description: s.description || "",
141
+ type: s.type || (s.$ref ? "ref" : "object"),
142
+ required: s.required || [],
143
+ properties: summarizeProperties(s, spec) || {},
144
+ }))
145
+
146
+ return { ok: true, count: models.length, models }
147
+ }
148
+
149
+ const MODEL_TOOLS = [
150
+ {
151
+ name: "describe_data_models",
152
+ description:
153
+ "Enumerate or detail OpenAPI data models from components.schemas. With no args, returns every schema with a property summary. Pass `name` for one exact-match model, or `query` for a case-insensitive substring filter across model name + description. Property summaries walk allOf and resolve $ref by name so referenced models are visible without a second call.",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ name: {
158
+ type: "string",
159
+ description:
160
+ "Exact model name (e.g. 'Attendee'). Returns only that model.",
161
+ },
162
+ query: {
163
+ type: "string",
164
+ description:
165
+ "Case-insensitive substring filter across model name + description.",
166
+ },
167
+ },
168
+ },
169
+ },
170
+ ]
171
+
172
+ async function handleModelToolCall(name, args = {}) {
173
+ switch (name) {
174
+ case "describe_data_models":
175
+ return contentResult(await describeDataModels(args))
176
+ default:
177
+ throw new Error(`Unknown model tool: ${name}`)
178
+ }
179
+ }
180
+
181
+ function isModelTool(name) {
182
+ return MODEL_TOOLS.some((t) => t.name === name)
183
+ }
184
+
185
+ module.exports = {
186
+ MODEL_TOOLS,
187
+ handleModelToolCall,
188
+ isModelTool,
189
+ describeDataModels,
190
+ summarizeProperties,
191
+ resolveRef,
192
+ }
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Shared MCP server runner.
3
+ *
4
+ * Used by both the primary `mcp-serve` bin and the deprecated
5
+ * `gxp-api-server` shim. Wires the official @modelcontextprotocol/sdk
6
+ * over StdioServerTransport. The SDK is ESM-only and this module is
7
+ * CommonJS, so the SDK is loaded via dynamic import() inside startServer().
8
+ *
9
+ * The full tool surface lives here:
10
+ * - API spec tools (openapi/asyncapi search, endpoint details, env)
11
+ * - Extended API tools (api-tools.js)
12
+ * - Config tools (config-tools.js)
13
+ * - Docs tools (docs-tools.js)
14
+ * - Test tools (test-tools.js)
15
+ * - Model tools (model-tools.js)
16
+ * - UIKit tools (uikit-tools.js)
17
+ */
18
+
19
+ const { fetchSpec, getEnvironment, getEnvUrls } = require("./specs")
20
+ const {
21
+ CONFIG_TOOLS,
22
+ handleConfigToolCall,
23
+ isConfigTool,
24
+ } = require("./config-tools")
25
+ const {
26
+ EXT_API_TOOLS,
27
+ handleExtApiToolCall,
28
+ isExtApiTool,
29
+ } = require("./api-tools")
30
+ const { DOCS_TOOLS, handleDocsToolCall, isDocsTool } = require("./docs-tools")
31
+ const { TEST_TOOLS, handleTestToolCall, isTestTool } = require("./test-tools")
32
+ const {
33
+ MODEL_TOOLS,
34
+ handleModelToolCall,
35
+ isModelTool,
36
+ } = require("./model-tools")
37
+ const {
38
+ UIKIT_TOOLS,
39
+ handleUikitToolCall,
40
+ isUikitTool,
41
+ } = require("./uikit-tools")
42
+
43
+ const SERVER_INFO = {
44
+ name: "gxp-mcp-serve",
45
+ version: "2.1.0",
46
+ }
47
+
48
+ const SERVER_DESCRIPTION =
49
+ "GxP toolkit MCP server: API specs, data models, UIKit components, config/manifest editing, documentation search, and plugin test helpers for AI coding assistants."
50
+
51
+ /* -------------------- API spec search helpers (in-file) ------------------- */
52
+
53
+ function searchEndpoints(spec, query) {
54
+ const results = []
55
+ const queryLower = String(query).toLowerCase()
56
+
57
+ if (spec.paths) {
58
+ for (const [p, methods] of Object.entries(spec.paths)) {
59
+ for (const [method, details] of Object.entries(methods)) {
60
+ if (
61
+ typeof details === "object" &&
62
+ (p.toLowerCase().includes(queryLower) ||
63
+ details.summary?.toLowerCase().includes(queryLower) ||
64
+ details.description?.toLowerCase().includes(queryLower) ||
65
+ details.operationId?.toLowerCase().includes(queryLower) ||
66
+ details.tags?.some((t) => t.toLowerCase().includes(queryLower)))
67
+ ) {
68
+ results.push({
69
+ path: p,
70
+ method: method.toUpperCase(),
71
+ summary: details.summary || "",
72
+ description: details.description || "",
73
+ operationId: details.operationId || "",
74
+ tags: details.tags || [],
75
+ parameters: details.parameters || [],
76
+ requestBody: details.requestBody || null,
77
+ responses: Object.keys(details.responses || {}),
78
+ })
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ return results
85
+ }
86
+
87
+ function searchEvents(spec, query) {
88
+ const results = []
89
+ const queryLower = String(query).toLowerCase()
90
+
91
+ const messages = spec?.components?.messages || {}
92
+ for (const [eventName, message] of Object.entries(messages)) {
93
+ if (typeof message !== "object" || message === null) continue
94
+ const trigger = message["x-triggered-by"] || ""
95
+ if (
96
+ eventName.toLowerCase().includes(queryLower) ||
97
+ message.summary?.toLowerCase().includes(queryLower) ||
98
+ message.description?.toLowerCase().includes(queryLower) ||
99
+ trigger.toLowerCase().includes(queryLower)
100
+ ) {
101
+ results.push({
102
+ kind: "event",
103
+ eventName,
104
+ summary: message.summary || "",
105
+ description: message.description || "",
106
+ triggeredBy: trigger || null,
107
+ payloadRef: message.payload?.$ref || null,
108
+ })
109
+ }
110
+ }
111
+
112
+ if (spec.channels) {
113
+ for (const [channel, details] of Object.entries(spec.channels)) {
114
+ if (
115
+ channel.toLowerCase().includes(queryLower) ||
116
+ details.description?.toLowerCase().includes(queryLower)
117
+ ) {
118
+ const operations = []
119
+ if (details.publish) {
120
+ operations.push({
121
+ type: "publish",
122
+ summary: details.publish.summary || "",
123
+ message: details.publish.message || null,
124
+ })
125
+ }
126
+ if (details.subscribe) {
127
+ operations.push({
128
+ type: "subscribe",
129
+ summary: details.subscribe.summary || "",
130
+ message: details.subscribe.message || null,
131
+ })
132
+ }
133
+
134
+ results.push({
135
+ kind: "channel",
136
+ channel,
137
+ description: details.description || "",
138
+ operations,
139
+ })
140
+ }
141
+ }
142
+ }
143
+
144
+ return results
145
+ }
146
+
147
+ function getEndpointDetails(spec, p, method) {
148
+ const methodLower = method.toLowerCase()
149
+ const endpoint = spec.paths?.[p]?.[methodLower]
150
+
151
+ if (!endpoint) {
152
+ return null
153
+ }
154
+
155
+ return {
156
+ path: p,
157
+ method: method.toUpperCase(),
158
+ summary: endpoint.summary || "",
159
+ description: endpoint.description || "",
160
+ operationId: endpoint.operationId || "",
161
+ tags: endpoint.tags || [],
162
+ parameters: endpoint.parameters || [],
163
+ requestBody: endpoint.requestBody || null,
164
+ responses: endpoint.responses || {},
165
+ security: endpoint.security || spec.security || [],
166
+ }
167
+ }
168
+
169
+ /* ------------------------------- tool schemas ----------------------------- */
170
+
171
+ const API_TOOLS = [
172
+ {
173
+ name: "get_openapi_spec",
174
+ description:
175
+ "Fetch the full OpenAPI specification for the GxP API. Returns the complete spec including all endpoints, schemas, and documentation.",
176
+ inputSchema: { type: "object", properties: {}, required: [] },
177
+ },
178
+ {
179
+ name: "get_asyncapi_spec",
180
+ description:
181
+ "Fetch the AsyncAPI specification for GxP WebSocket events. Returns channel definitions, message schemas, and event documentation.",
182
+ inputSchema: { type: "object", properties: {}, required: [] },
183
+ },
184
+ {
185
+ name: "search_api_endpoints",
186
+ description:
187
+ "Search for API endpoints matching a query. Searches path, summary, description, operation ID, and tags.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ query: {
192
+ type: "string",
193
+ description:
194
+ "Search term to find matching endpoints (e.g., 'attendee', 'check-in', 'event')",
195
+ },
196
+ },
197
+ required: ["query"],
198
+ },
199
+ },
200
+ {
201
+ name: "search_websocket_events",
202
+ description:
203
+ "Search AsyncAPI events matching a query. Searches components.messages (event name, summary, description, x-triggered-by) and channel definitions. The returned eventName is what you pass to store.listen(eventName, permissionIdentifier, callback).",
204
+ inputSchema: {
205
+ type: "object",
206
+ properties: {
207
+ query: {
208
+ type: "string",
209
+ description:
210
+ "Search term to find matching events (e.g., 'message', 'created', 'updated')",
211
+ },
212
+ },
213
+ required: ["query"],
214
+ },
215
+ },
216
+ {
217
+ name: "get_endpoint_details",
218
+ description:
219
+ "Get detailed information about a specific API endpoint including parameters, request body, and responses.",
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ path: {
224
+ type: "string",
225
+ description: "API endpoint path (e.g., '/api/v1/attendees')",
226
+ },
227
+ method: {
228
+ type: "string",
229
+ description: "HTTP method (GET, POST, PUT, PATCH, DELETE)",
230
+ },
231
+ },
232
+ required: ["path", "method"],
233
+ },
234
+ },
235
+ {
236
+ name: "get_api_environment",
237
+ description:
238
+ "Get the current API environment configuration including base URL and spec URLs.",
239
+ inputSchema: { type: "object", properties: {}, required: [] },
240
+ },
241
+ ]
242
+
243
+ const TOOLS = [
244
+ ...API_TOOLS,
245
+ ...EXT_API_TOOLS,
246
+ ...CONFIG_TOOLS,
247
+ ...DOCS_TOOLS,
248
+ ...TEST_TOOLS,
249
+ ...MODEL_TOOLS,
250
+ ...UIKIT_TOOLS,
251
+ ]
252
+
253
+ /* ------------------------------ tool dispatch ----------------------------- */
254
+
255
+ async function handleToolCall(name, args = {}) {
256
+ if (isConfigTool(name)) return handleConfigToolCall(name, args)
257
+ if (isExtApiTool(name)) return handleExtApiToolCall(name, args)
258
+ if (isDocsTool(name)) return handleDocsToolCall(name, args)
259
+ if (isTestTool(name)) return handleTestToolCall(name, args)
260
+ if (isModelTool(name)) return handleModelToolCall(name, args)
261
+ if (isUikitTool(name)) return handleUikitToolCall(name, args)
262
+
263
+ switch (name) {
264
+ case "get_openapi_spec": {
265
+ const spec = await fetchSpec("openapi")
266
+ return {
267
+ content: [{ type: "text", text: JSON.stringify(spec, null, 2) }],
268
+ }
269
+ }
270
+ case "get_asyncapi_spec": {
271
+ const spec = await fetchSpec("asyncapi")
272
+ return {
273
+ content: [{ type: "text", text: JSON.stringify(spec, null, 2) }],
274
+ }
275
+ }
276
+ case "search_api_endpoints": {
277
+ const spec = await fetchSpec("openapi")
278
+ const results = searchEndpoints(spec, args.query)
279
+ return {
280
+ content: [
281
+ {
282
+ type: "text",
283
+ text:
284
+ results.length > 0
285
+ ? JSON.stringify(results, null, 2)
286
+ : `No endpoints found matching "${args.query}"`,
287
+ },
288
+ ],
289
+ }
290
+ }
291
+ case "search_websocket_events": {
292
+ const spec = await fetchSpec("asyncapi")
293
+ const results = searchEvents(spec, args.query)
294
+ return {
295
+ content: [
296
+ {
297
+ type: "text",
298
+ text:
299
+ results.length > 0
300
+ ? JSON.stringify(results, null, 2)
301
+ : `No events found matching "${args.query}"`,
302
+ },
303
+ ],
304
+ }
305
+ }
306
+ case "get_endpoint_details": {
307
+ const spec = await fetchSpec("openapi")
308
+ const details = getEndpointDetails(spec, args.path, args.method)
309
+ return {
310
+ content: [
311
+ {
312
+ type: "text",
313
+ text: details
314
+ ? JSON.stringify(details, null, 2)
315
+ : `Endpoint not found: ${args.method} ${args.path}`,
316
+ },
317
+ ],
318
+ }
319
+ }
320
+ case "get_api_environment": {
321
+ const env = getEnvironment()
322
+ const urls = getEnvUrls()
323
+ return {
324
+ content: [
325
+ {
326
+ type: "text",
327
+ text: JSON.stringify({ environment: env, ...urls }, null, 2),
328
+ },
329
+ ],
330
+ }
331
+ }
332
+ default:
333
+ throw new Error(`Unknown tool: ${name}`)
334
+ }
335
+ }
336
+
337
+ /* ------------------------------- entry point ------------------------------ */
338
+
339
+ /**
340
+ * Boot the MCP server over stdio. Loads the official SDK via dynamic import
341
+ * because @modelcontextprotocol/sdk is ESM-only and this package is CJS.
342
+ */
343
+ async function startServer() {
344
+ const sdkServer = await import("@modelcontextprotocol/sdk/server/index.js")
345
+ const sdkStdio = await import("@modelcontextprotocol/sdk/server/stdio.js")
346
+ const sdkTypes = await import("@modelcontextprotocol/sdk/types.js")
347
+ const { Server } = sdkServer
348
+ const { StdioServerTransport } = sdkStdio
349
+ const { ListToolsRequestSchema, CallToolRequestSchema } = sdkTypes
350
+
351
+ const server = new Server(
352
+ { name: SERVER_INFO.name, version: SERVER_INFO.version },
353
+ { capabilities: { tools: {} } },
354
+ )
355
+
356
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
357
+ tools: TOOLS,
358
+ }))
359
+
360
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
361
+ const { name, arguments: args } = request.params
362
+ try {
363
+ return await handleToolCall(name, args || {})
364
+ } catch (err) {
365
+ return {
366
+ isError: true,
367
+ content: [
368
+ {
369
+ type: "text",
370
+ text: `Error: ${err && err.message ? err.message : String(err)}`,
371
+ },
372
+ ],
373
+ }
374
+ }
375
+ })
376
+
377
+ const transport = new StdioServerTransport()
378
+ await server.connect(transport)
379
+ }
380
+
381
+ module.exports = {
382
+ startServer,
383
+ TOOLS,
384
+ API_TOOLS,
385
+ SERVER_INFO,
386
+ SERVER_DESCRIPTION,
387
+ handleToolCall,
388
+ searchEndpoints,
389
+ searchEvents,
390
+ getEndpointDetails,
391
+ }