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.5" }, { capabilities: { tools: {} } });
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
- const inputSchema = {
327
- $defs: this.convertComponentsToJsonSchema(),
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magicpod-mcp-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Model Context Protocol server for MagicPod integration",
5
5
  "type": "module",
6
6
  "bin": {