@gxp-dev/tools 2.0.82 → 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.
- package/README.md +7 -3
- package/bin/lib/cli.js +4 -2
- package/bin/lib/commands/init.js +3 -2
- package/bin/lib/utils/ai-scaffold.js +10 -8
- package/mcp/gxp-api-server.js +19 -475
- package/mcp/lib/model-tools.js +192 -0
- package/mcp/lib/server.js +391 -0
- package/mcp/lib/uikit-tools.js +181 -0
- package/mcp/mcp-serve.js +25 -0
- package/package.json +3 -1
- package/template/.claude/agents/gxp-developer.md +1 -1
- package/template/AGENTS.md +1 -1
- package/template/GEMINI.md +1 -1
- package/template/gemini/settings.json +1 -1
- package/template/mcp.json +1 -1
|
@@ -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
|
+
}
|