magicpod-mcp-server 0.1.5 → 0.1.6
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.
|
@@ -12,7 +12,7 @@ export class MCPProxy {
|
|
|
12
12
|
openApiLookup;
|
|
13
13
|
constructor(name, openApiSpec, apiToken, otherTools) {
|
|
14
14
|
this.otherTools = otherTools;
|
|
15
|
-
this.server = new Server({ name, version: "0.1.
|
|
15
|
+
this.server = new Server({ name, version: "0.1.6" }, { capabilities: { tools: {} } });
|
|
16
16
|
const baseUrl = openApiSpec.servers?.[0].url;
|
|
17
17
|
if (!baseUrl) {
|
|
18
18
|
throw new Error("No base URL found in OpenAPI spec");
|
|
@@ -48,30 +48,6 @@ export class MCPProxy {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
collectRefs(obj) {
|
|
52
|
-
const refs = [];
|
|
53
|
-
if (Array.isArray(obj)) {
|
|
54
|
-
for (const childRefs of obj.map(this.collectRefs)) {
|
|
55
|
-
for (const childRef of childRefs) {
|
|
56
|
-
refs.push(childRef);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
else if (obj && typeof obj === "object") {
|
|
61
|
-
for (const key in obj) {
|
|
62
|
-
if (key === "$ref") {
|
|
63
|
-
// @ts-ignore
|
|
64
|
-
const ref = obj[key];
|
|
65
|
-
refs.push(ref.replaceAll("#/$defs/", ""));
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
// @ts-ignore
|
|
69
|
-
this.collectRefs(obj[key]).forEach(refs.push);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return refs;
|
|
74
|
-
}
|
|
75
51
|
setupHandlers() {
|
|
76
52
|
// Handle tool listing
|
|
77
53
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -85,24 +61,6 @@ export class MCPProxy {
|
|
|
85
61
|
// TODO description is actually required
|
|
86
62
|
const inputSchema = JSON.parse(JSON.stringify(method.inputSchema));
|
|
87
63
|
this.removeDescriptions(inputSchema);
|
|
88
|
-
// 95% of the response size is consumed by $defs
|
|
89
|
-
const body = method.inputSchema.properties?.body;
|
|
90
|
-
if (body == null || typeof body === "boolean") {
|
|
91
|
-
delete inputSchema["$defs"];
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
const refs = this.collectRefs(body);
|
|
95
|
-
if (refs.length === 0) {
|
|
96
|
-
delete inputSchema["$defs"];
|
|
97
|
-
}
|
|
98
|
-
else if (inputSchema["$defs"]) {
|
|
99
|
-
for (const def of Object.keys(inputSchema["$defs"])) {
|
|
100
|
-
if (!refs.includes(def)) {
|
|
101
|
-
delete inputSchema["$defs"][def];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
64
|
tools.push({
|
|
107
65
|
name: truncatedToolName,
|
|
108
66
|
description: method.description,
|
|
@@ -26,6 +26,100 @@ export class OpenAPIToMCPConverter {
|
|
|
26
26
|
resolvedRefs.add(ref);
|
|
27
27
|
return current;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Inline all $ref references in a schema by replacing them with their actual definitions.
|
|
31
|
+
* Handles circular references by detecting cycles and breaking them with simplified schemas.
|
|
32
|
+
*/
|
|
33
|
+
inlineAllRefs(schema, resolutionStack = new Set(), inlinedCache = new Map()) {
|
|
34
|
+
if (!schema || typeof schema !== "object") {
|
|
35
|
+
return schema;
|
|
36
|
+
}
|
|
37
|
+
// Handle $ref references
|
|
38
|
+
if ("$ref" in schema && typeof schema.$ref === "string") {
|
|
39
|
+
const ref = schema.$ref;
|
|
40
|
+
// Check for circular reference
|
|
41
|
+
if (resolutionStack.has(ref)) {
|
|
42
|
+
console.warn(`Circular reference detected for ${ref}, using simplified schema`);
|
|
43
|
+
return {
|
|
44
|
+
type: "object",
|
|
45
|
+
description: `Circular reference to ${ref}`,
|
|
46
|
+
additionalProperties: true
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Check cache first
|
|
50
|
+
if (inlinedCache.has(ref)) {
|
|
51
|
+
return inlinedCache.get(ref);
|
|
52
|
+
}
|
|
53
|
+
// Convert #/$defs/ references back to #/components/schemas/ for resolution
|
|
54
|
+
let resolveRef = ref;
|
|
55
|
+
if (ref.startsWith("#/$defs/")) {
|
|
56
|
+
resolveRef = ref.replace(/^#\/\$defs\//, "#/components/schemas/");
|
|
57
|
+
}
|
|
58
|
+
// Resolve the reference
|
|
59
|
+
const resolved = this.internalResolveRef(resolveRef, new Set());
|
|
60
|
+
if (!resolved) {
|
|
61
|
+
console.error(`Failed to resolve reference ${ref} (tried as ${resolveRef})`);
|
|
62
|
+
return {
|
|
63
|
+
type: "object",
|
|
64
|
+
description: `Unresolved reference: ${ref}`,
|
|
65
|
+
additionalProperties: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Convert and inline recursively
|
|
69
|
+
resolutionStack.add(ref);
|
|
70
|
+
const convertedResolved = this.convertOpenApiSchemaToJsonSchema(resolved, new Set(), true);
|
|
71
|
+
const inlinedResolved = this.inlineAllRefs(convertedResolved, resolutionStack, inlinedCache);
|
|
72
|
+
resolutionStack.delete(ref);
|
|
73
|
+
// Preserve description from the original $ref if it exists
|
|
74
|
+
if (schema.description && !inlinedResolved.description) {
|
|
75
|
+
inlinedResolved.description = schema.description;
|
|
76
|
+
}
|
|
77
|
+
// Cache the result
|
|
78
|
+
inlinedCache.set(ref, inlinedResolved);
|
|
79
|
+
return inlinedResolved;
|
|
80
|
+
}
|
|
81
|
+
// Handle arrays
|
|
82
|
+
if (schema.type === "array" && schema.items) {
|
|
83
|
+
return {
|
|
84
|
+
...schema,
|
|
85
|
+
items: this.inlineAllRefs(schema.items, resolutionStack, inlinedCache)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Handle objects with properties
|
|
89
|
+
if (schema.type === "object" && schema.properties) {
|
|
90
|
+
const inlinedProperties = {};
|
|
91
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
92
|
+
inlinedProperties[key] = this.inlineAllRefs(value, resolutionStack, inlinedCache);
|
|
93
|
+
}
|
|
94
|
+
const result = { ...schema, properties: inlinedProperties };
|
|
95
|
+
// Handle additionalProperties
|
|
96
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
97
|
+
result.additionalProperties = this.inlineAllRefs(schema.additionalProperties, resolutionStack, inlinedCache);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
// Handle oneOf, anyOf, allOf
|
|
102
|
+
if (schema.oneOf) {
|
|
103
|
+
return {
|
|
104
|
+
...schema,
|
|
105
|
+
oneOf: schema.oneOf.map(s => this.inlineAllRefs(s, resolutionStack, inlinedCache))
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (schema.anyOf) {
|
|
109
|
+
return {
|
|
110
|
+
...schema,
|
|
111
|
+
anyOf: schema.anyOf.map(s => this.inlineAllRefs(s, resolutionStack, inlinedCache))
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (schema.allOf) {
|
|
115
|
+
return {
|
|
116
|
+
...schema,
|
|
117
|
+
allOf: schema.allOf.map(s => this.inlineAllRefs(s, resolutionStack, inlinedCache))
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Return schema as-is if no special handling needed
|
|
121
|
+
return schema;
|
|
122
|
+
}
|
|
29
123
|
/**
|
|
30
124
|
* Convert an OpenAPI schema (or reference) into a JSON Schema object.
|
|
31
125
|
* Uses caching and handles cycles by returning $ref nodes.
|
|
@@ -226,6 +320,78 @@ export class OpenAPIToMCPConverter {
|
|
|
226
320
|
}
|
|
227
321
|
return schema;
|
|
228
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Create a fully inlined schema without any $ref or $defs for Gemini CLI compatibility
|
|
325
|
+
*/
|
|
326
|
+
createInlinedSchema(operation, method, path) {
|
|
327
|
+
// First create the schema with $defs
|
|
328
|
+
const schemaWithRefs = {
|
|
329
|
+
$defs: this.convertComponentsToJsonSchema(),
|
|
330
|
+
type: "object",
|
|
331
|
+
properties: {},
|
|
332
|
+
required: [],
|
|
333
|
+
};
|
|
334
|
+
// Handle parameters (path, query, header, cookie)
|
|
335
|
+
if (operation.parameters) {
|
|
336
|
+
for (const param of operation.parameters) {
|
|
337
|
+
const paramObj = this.resolveParameter(param);
|
|
338
|
+
if (paramObj && paramObj.schema) {
|
|
339
|
+
const schema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set(), false);
|
|
340
|
+
// Merge parameter-level description if available
|
|
341
|
+
if (paramObj.description) {
|
|
342
|
+
schema.description = paramObj.description;
|
|
343
|
+
}
|
|
344
|
+
schemaWithRefs.properties[paramObj.name] = schema;
|
|
345
|
+
if (paramObj.required) {
|
|
346
|
+
schemaWithRefs.required.push(paramObj.name);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Handle requestBody
|
|
352
|
+
if (operation.requestBody) {
|
|
353
|
+
const bodyObj = this.resolveRequestBody(operation.requestBody);
|
|
354
|
+
if (bodyObj?.content) {
|
|
355
|
+
// Handle multipart/form-data for file uploads
|
|
356
|
+
if (bodyObj.content["multipart/form-data"]?.schema) {
|
|
357
|
+
const formSchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content["multipart/form-data"].schema, new Set(), false);
|
|
358
|
+
if (formSchema.type === "object" && formSchema.properties) {
|
|
359
|
+
for (const [name, propSchema] of Object.entries(formSchema.properties)) {
|
|
360
|
+
schemaWithRefs.properties[name] = propSchema;
|
|
361
|
+
}
|
|
362
|
+
if (formSchema.required) {
|
|
363
|
+
schemaWithRefs.required.push(...formSchema.required);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Handle application/json
|
|
368
|
+
else if (bodyObj.content["application/json"]?.schema) {
|
|
369
|
+
const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content["application/json"].schema, new Set(), false);
|
|
370
|
+
// Merge body schema into the inputSchema's properties
|
|
371
|
+
if (bodySchema.type === "object" && bodySchema.properties) {
|
|
372
|
+
for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
|
|
373
|
+
schemaWithRefs.properties[name] = propSchema;
|
|
374
|
+
}
|
|
375
|
+
if (bodySchema.required) {
|
|
376
|
+
schemaWithRefs.required.push(...bodySchema.required);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// If the request body is not an object, just put it under "body"
|
|
381
|
+
schemaWithRefs.properties["body"] = bodySchema;
|
|
382
|
+
schemaWithRefs.required.push("body");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Now inline all references and remove $defs
|
|
388
|
+
const inlinedSchema = this.inlineAllRefs(schemaWithRefs);
|
|
389
|
+
// Ensure we remove $defs completely for Gemini CLI compatibility
|
|
390
|
+
if ("$defs" in inlinedSchema) {
|
|
391
|
+
delete inlinedSchema["$defs"];
|
|
392
|
+
}
|
|
393
|
+
return inlinedSchema;
|
|
394
|
+
}
|
|
229
395
|
/**
|
|
230
396
|
* Helper method to convert an operation to a JSON Schema for parameters
|
|
231
397
|
*/
|
|
@@ -323,67 +489,8 @@ export class OpenAPIToMCPConverter {
|
|
|
323
489
|
return null;
|
|
324
490
|
}
|
|
325
491
|
const methodName = operation.operationId.replaceAll("\.", "_");
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
type: "object",
|
|
329
|
-
properties: {},
|
|
330
|
-
required: [],
|
|
331
|
-
};
|
|
332
|
-
// Handle parameters (path, query, header, cookie)
|
|
333
|
-
if (operation.parameters) {
|
|
334
|
-
for (const param of operation.parameters) {
|
|
335
|
-
const paramObj = this.resolveParameter(param);
|
|
336
|
-
if (paramObj && paramObj.schema) {
|
|
337
|
-
const schema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set(), false);
|
|
338
|
-
// Merge parameter-level description if available
|
|
339
|
-
if (paramObj.description) {
|
|
340
|
-
schema.description = paramObj.description;
|
|
341
|
-
}
|
|
342
|
-
inputSchema.properties[paramObj.name] = schema;
|
|
343
|
-
if (paramObj.required) {
|
|
344
|
-
inputSchema.required.push(paramObj.name);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
// Handle requestBody
|
|
350
|
-
if (operation.requestBody) {
|
|
351
|
-
const bodyObj = this.resolveRequestBody(operation.requestBody);
|
|
352
|
-
if (bodyObj?.content) {
|
|
353
|
-
// Handle multipart/form-data for file uploads
|
|
354
|
-
// We convert the multipart/form-data schema to a JSON schema and we require
|
|
355
|
-
// that the user passes in a string for each file that points to the local file
|
|
356
|
-
if (bodyObj.content["multipart/form-data"]?.schema) {
|
|
357
|
-
const formSchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content["multipart/form-data"].schema, new Set(), false);
|
|
358
|
-
if (formSchema.type === "object" && formSchema.properties) {
|
|
359
|
-
for (const [name, propSchema] of Object.entries(formSchema.properties)) {
|
|
360
|
-
inputSchema.properties[name] = propSchema;
|
|
361
|
-
}
|
|
362
|
-
if (formSchema.required) {
|
|
363
|
-
inputSchema.required.push(...formSchema.required);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// Handle application/json
|
|
368
|
-
else if (bodyObj.content["application/json"]?.schema) {
|
|
369
|
-
const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content["application/json"].schema, new Set(), false);
|
|
370
|
-
// Merge body schema into the inputSchema's properties
|
|
371
|
-
if (bodySchema.type === "object" && bodySchema.properties) {
|
|
372
|
-
for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
|
|
373
|
-
inputSchema.properties[name] = propSchema;
|
|
374
|
-
}
|
|
375
|
-
if (bodySchema.required) {
|
|
376
|
-
inputSchema.required.push(...bodySchema.required);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
// If the request body is not an object, just put it under "body"
|
|
381
|
-
inputSchema.properties["body"] = bodySchema;
|
|
382
|
-
inputSchema.required.push("body");
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
492
|
+
// Use the inlined schema method for Gemini CLI compatibility
|
|
493
|
+
const inputSchema = this.createInlinedSchema(operation, method, path);
|
|
387
494
|
// Build description including error responses
|
|
388
495
|
let description = operation.summary || operation.description || "";
|
|
389
496
|
if (operation.responses) {
|
|
@@ -30,6 +30,7 @@ export const searchMagicpodArticles = () => {
|
|
|
30
30
|
.describe("Query's and search target's locale"),
|
|
31
31
|
}),
|
|
32
32
|
handleRequest: async ({ query, locale }) => {
|
|
33
|
+
query += ' -label:release';
|
|
33
34
|
const response = await makeRequest(query, locale);
|
|
34
35
|
// The response has "body" field, but it is too large for LLM
|
|
35
36
|
// So, such large or insignificant fields are filtered here
|