spec2openapi 0.1.0__py3-none-any.whl
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.
- spec2openapi/__init__.py +55 -0
- spec2openapi/bridge.py +478 -0
- spec2openapi/cli.py +330 -0
- spec2openapi/convert.py +62 -0
- spec2openapi/openapi.py +234 -0
- spec2openapi/parser.py +403 -0
- spec2openapi/schema.py +398 -0
- spec2openapi/server.py +85 -0
- spec2openapi/swagger.py +430 -0
- spec2openapi-0.1.0.dist-info/METADATA +209 -0
- spec2openapi-0.1.0.dist-info/RECORD +16 -0
- spec2openapi-0.1.0.dist-info/WHEEL +5 -0
- spec2openapi-0.1.0.dist-info/entry_points.txt +2 -0
- spec2openapi-0.1.0.dist-info/licenses/LICENSE +202 -0
- spec2openapi-0.1.0.dist-info/licenses/NOTICE +2 -0
- spec2openapi-0.1.0.dist-info/top_level.txt +1 -0
spec2openapi/schema.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Convert zeep XSD types into JSON Schema (with OpenAPI `xml` annotations).
|
|
2
|
+
|
|
3
|
+
The generated schemas serve two purposes:
|
|
4
|
+
1. They describe operation inputs/outputs for FastMCP / OpenAPI tooling
|
|
5
|
+
(descriptions, enums, patterns and bounds improve LLM tool usage).
|
|
6
|
+
2. Their `xml` annotations carry enough metadata for a SOAP call layer to
|
|
7
|
+
serialize JSON back into a literal XML payload (element names,
|
|
8
|
+
namespaces, attribute markers, text content, ordering = property
|
|
9
|
+
insertion order).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import datetime
|
|
14
|
+
import decimal
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from zeep import xsd as zx
|
|
20
|
+
|
|
21
|
+
from .parser import XsdMeta
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("spec2openapi")
|
|
24
|
+
|
|
25
|
+
# format hints derived from zeep builtin class names found in the type's MRO
|
|
26
|
+
_FORMAT_BY_CLS = {
|
|
27
|
+
"DateTime": "date-time",
|
|
28
|
+
"Date": "date",
|
|
29
|
+
"Time": "time",
|
|
30
|
+
"Base64Binary": "byte",
|
|
31
|
+
"AnyURI": "uri",
|
|
32
|
+
"Duration": "duration",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_NAME_RE = re.compile(r"[^A-Za-z0-9_.-]")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sanitize_name(name: str) -> str:
|
|
39
|
+
out = _NAME_RE.sub("_", name or "unnamed")
|
|
40
|
+
return out[:64] or "unnamed"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _py_to_json_type(py: type) -> dict[str, Any]:
|
|
44
|
+
if py is bool:
|
|
45
|
+
return {"type": "boolean"}
|
|
46
|
+
if py is int:
|
|
47
|
+
return {"type": "integer"}
|
|
48
|
+
if py in (float, decimal.Decimal):
|
|
49
|
+
return {"type": "number"}
|
|
50
|
+
if py is datetime.datetime:
|
|
51
|
+
return {"type": "string", "format": "date-time"}
|
|
52
|
+
if py is datetime.date:
|
|
53
|
+
return {"type": "string", "format": "date"}
|
|
54
|
+
if py is datetime.time:
|
|
55
|
+
return {"type": "string", "format": "time"}
|
|
56
|
+
return {"type": "string"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _coerce_default(value: str, schema: dict[str, Any]) -> Any:
|
|
60
|
+
t = schema.get("type")
|
|
61
|
+
try:
|
|
62
|
+
if t == "integer":
|
|
63
|
+
return int(value)
|
|
64
|
+
if t == "number":
|
|
65
|
+
return float(value)
|
|
66
|
+
if t == "boolean":
|
|
67
|
+
return value in ("true", "1")
|
|
68
|
+
except ValueError:
|
|
69
|
+
pass
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _choice_groups(t: Any) -> list[dict[str, Any]]:
|
|
74
|
+
"""Walk zeep's indicator tree to find xsd:choice member groups."""
|
|
75
|
+
groups: list[dict[str, Any]] = []
|
|
76
|
+
|
|
77
|
+
def member_names(indicator) -> list[str]:
|
|
78
|
+
names = []
|
|
79
|
+
for item in indicator:
|
|
80
|
+
if isinstance(item, tuple) and item:
|
|
81
|
+
names.append(str(item[0]))
|
|
82
|
+
else:
|
|
83
|
+
n = getattr(item, "name", None)
|
|
84
|
+
if n:
|
|
85
|
+
names.append(n)
|
|
86
|
+
return names
|
|
87
|
+
|
|
88
|
+
def walk(indicator):
|
|
89
|
+
try:
|
|
90
|
+
items = list(indicator)
|
|
91
|
+
except TypeError:
|
|
92
|
+
return
|
|
93
|
+
for item in items:
|
|
94
|
+
cls = type(item).__name__
|
|
95
|
+
if cls == "Choice":
|
|
96
|
+
names = member_names(item)
|
|
97
|
+
if names:
|
|
98
|
+
try:
|
|
99
|
+
required = int(getattr(item, "min_occurs", 1)) >= 1
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
required = True
|
|
102
|
+
groups.append({"members": names, "required": required})
|
|
103
|
+
elif cls in ("Sequence", "All", "Group"):
|
|
104
|
+
walk(item)
|
|
105
|
+
|
|
106
|
+
root = getattr(t, "_element", None)
|
|
107
|
+
if root is not None:
|
|
108
|
+
walk(root)
|
|
109
|
+
return groups
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SchemaConverter:
|
|
113
|
+
"""Stateful converter that accumulates shared component schemas."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, meta: XsdMeta | None = None):
|
|
116
|
+
self.meta = meta or XsdMeta(facets={}, type_docs={}, child_docs={})
|
|
117
|
+
self.components: dict[str, dict[str, Any]] = {}
|
|
118
|
+
self._registered: dict[int, str] = {} # id(zeep type) -> component name
|
|
119
|
+
self._in_progress: dict[int, str] = {}
|
|
120
|
+
|
|
121
|
+
# -- metadata lookups -------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def _lookup(self, table: dict, ns: str | None, name: str | None):
|
|
124
|
+
if not name:
|
|
125
|
+
return None
|
|
126
|
+
if ns is not None and (ns, name) in table:
|
|
127
|
+
return table[(ns, name)]
|
|
128
|
+
for key, value in table.items():
|
|
129
|
+
if key[-1] == name or key[1] == name:
|
|
130
|
+
if len(key) == 2 and key[1] == name:
|
|
131
|
+
return value
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def _child_doc(self, qkey: tuple[str, str] | None, child: str) -> str | None:
|
|
135
|
+
if qkey is None:
|
|
136
|
+
return None
|
|
137
|
+
ns, container = qkey
|
|
138
|
+
doc = self.meta.child_docs.get((ns, container, child))
|
|
139
|
+
if doc:
|
|
140
|
+
return doc
|
|
141
|
+
for (kns, kcont, kchild), value in self.meta.child_docs.items():
|
|
142
|
+
if kcont == container and kchild == child:
|
|
143
|
+
return value
|
|
144
|
+
# inherited elements (xsd:extension): the doc lives on the base type
|
|
145
|
+
for (kns, _kcont, kchild), value in self.meta.child_docs.items():
|
|
146
|
+
if kns == ns and kchild == child:
|
|
147
|
+
return value
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
def _type_qkey(self, t: Any) -> tuple[str | None, str | None]:
|
|
151
|
+
qname = getattr(t, "qname", None)
|
|
152
|
+
if qname is not None:
|
|
153
|
+
return qname.namespace, qname.localname
|
|
154
|
+
return None, getattr(t, "name", None)
|
|
155
|
+
|
|
156
|
+
# -- public ------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def element_type_to_object_schema(
|
|
159
|
+
self, xsd_type: Any, hint: str, qkey: tuple[str, str] | None = None
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""Schema for a wrapper element's complex type, always inlined as an
|
|
162
|
+
object (used for request/response bodies)."""
|
|
163
|
+
if isinstance(xsd_type, zx.ComplexType):
|
|
164
|
+
return self._complex_to_schema(xsd_type, hint, qkey=qkey)
|
|
165
|
+
return {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"properties": {"value": self._simple_to_schema(xsd_type)},
|
|
168
|
+
"required": ["value"],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def register_element_component(self, element: Any, hint: str) -> str:
|
|
172
|
+
"""Register a standalone element's type (headers, fault details)."""
|
|
173
|
+
t = getattr(element, "type", None)
|
|
174
|
+
if isinstance(t, zx.ComplexType):
|
|
175
|
+
return self._register_complex(t, hint)
|
|
176
|
+
name = sanitize_name(hint)
|
|
177
|
+
if name not in self.components:
|
|
178
|
+
self.components[name] = self._simple_to_schema(t)
|
|
179
|
+
return name
|
|
180
|
+
|
|
181
|
+
# -- internals -----------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def _type_to_schema(self, xsd_type: Any, hint: str) -> dict[str, Any]:
|
|
184
|
+
if isinstance(xsd_type, zx.ComplexType):
|
|
185
|
+
name = self._register_complex(xsd_type, hint)
|
|
186
|
+
return {"$ref": f"#/components/schemas/{name}"}
|
|
187
|
+
return self._simple_to_schema(xsd_type)
|
|
188
|
+
|
|
189
|
+
def _register_complex(self, t: Any, hint: str) -> str:
|
|
190
|
+
tid = id(t)
|
|
191
|
+
if tid in self._registered:
|
|
192
|
+
return self._registered[tid]
|
|
193
|
+
if tid in self._in_progress: # recursion cycle
|
|
194
|
+
return self._in_progress[tid]
|
|
195
|
+
|
|
196
|
+
base = sanitize_name(getattr(t, "name", None) or hint)
|
|
197
|
+
name = base
|
|
198
|
+
i = 2
|
|
199
|
+
while name in self.components:
|
|
200
|
+
name = f"{base}_{i}"
|
|
201
|
+
i += 1
|
|
202
|
+
self._in_progress[tid] = name
|
|
203
|
+
self.components[name] = {} # reserve slot to keep ordering stable
|
|
204
|
+
try:
|
|
205
|
+
tns, tname = self._type_qkey(t)
|
|
206
|
+
qkey = (tns or "", tname) if tname else None
|
|
207
|
+
self.components[name] = self._complex_to_schema(t, name, qkey=qkey)
|
|
208
|
+
self._registered[tid] = name
|
|
209
|
+
finally:
|
|
210
|
+
self._in_progress.pop(tid, None)
|
|
211
|
+
return name
|
|
212
|
+
|
|
213
|
+
def _complex_to_schema(
|
|
214
|
+
self, t: Any, hint: str, qkey: tuple[str, str] | None = None
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
props: dict[str, Any] = {}
|
|
217
|
+
required: list[str] = []
|
|
218
|
+
|
|
219
|
+
elements = list(getattr(t, "elements", []))
|
|
220
|
+
attributes = list(getattr(t, "attributes", []))
|
|
221
|
+
|
|
222
|
+
# xsd:simpleContent: a text value plus attributes
|
|
223
|
+
is_simple_content = (
|
|
224
|
+
len(elements) == 1
|
|
225
|
+
and elements[0][0] == "_value_1"
|
|
226
|
+
and not isinstance(elements[0][1].type, zx.ComplexType)
|
|
227
|
+
)
|
|
228
|
+
if is_simple_content:
|
|
229
|
+
value_schema = self._simple_to_schema(elements[0][1].type)
|
|
230
|
+
value_schema["xml"] = {"x-text": True}
|
|
231
|
+
value_schema.setdefault(
|
|
232
|
+
"description", "Text content of the element."
|
|
233
|
+
)
|
|
234
|
+
props["value"] = value_schema
|
|
235
|
+
required.append("value")
|
|
236
|
+
elements = []
|
|
237
|
+
|
|
238
|
+
for el_name, el in elements:
|
|
239
|
+
if el is None or type(el).__name__ in ("Any", "AnyObject"):
|
|
240
|
+
logger.debug("xsd:any inside %s -> additionalProperties", hint)
|
|
241
|
+
continue
|
|
242
|
+
prop = self._element_to_property(el_name, el, hint, qkey)
|
|
243
|
+
if prop is None:
|
|
244
|
+
continue
|
|
245
|
+
props[el_name] = prop
|
|
246
|
+
min_occurs = getattr(el, "min_occurs", 1)
|
|
247
|
+
try:
|
|
248
|
+
if int(min_occurs) >= 1:
|
|
249
|
+
required.append(el_name)
|
|
250
|
+
except (TypeError, ValueError):
|
|
251
|
+
required.append(el_name)
|
|
252
|
+
|
|
253
|
+
for at_name, attr in attributes:
|
|
254
|
+
if attr is None or type(attr).__name__ == "AnyAttribute":
|
|
255
|
+
continue
|
|
256
|
+
aschema = self._simple_to_schema(getattr(attr, "type", None))
|
|
257
|
+
aschema["xml"] = {"name": at_name, "attribute": True}
|
|
258
|
+
props[at_name] = aschema
|
|
259
|
+
if getattr(attr, "required", False):
|
|
260
|
+
required.append(at_name)
|
|
261
|
+
|
|
262
|
+
schema: dict[str, Any] = {"type": "object", "properties": props}
|
|
263
|
+
if required:
|
|
264
|
+
schema["required"] = required
|
|
265
|
+
|
|
266
|
+
# xsd:choice groups: members must not be required; record the groups
|
|
267
|
+
choices = _choice_groups(t)
|
|
268
|
+
if choices:
|
|
269
|
+
member_set = {m for g in choices for m in g["members"]}
|
|
270
|
+
if "required" in schema:
|
|
271
|
+
schema["required"] = [
|
|
272
|
+
r for r in schema["required"] if r not in member_set
|
|
273
|
+
]
|
|
274
|
+
if not schema["required"]:
|
|
275
|
+
del schema["required"]
|
|
276
|
+
schema["x-soap-choice"] = choices
|
|
277
|
+
notes = []
|
|
278
|
+
for g in choices:
|
|
279
|
+
kind = "Exactly one" if g["required"] else "At most one"
|
|
280
|
+
notes.append(f"{kind} of: {', '.join(g['members'])}.")
|
|
281
|
+
schema["description"] = " ".join(
|
|
282
|
+
filter(None, [schema.get("description"), *notes])
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if is_simple_content:
|
|
286
|
+
schema["x-soap-simple-content"] = True
|
|
287
|
+
|
|
288
|
+
# documentation from the raw XSD
|
|
289
|
+
tns, tname = self._type_qkey(t)
|
|
290
|
+
doc = self._lookup(self.meta.type_docs, tns, tname)
|
|
291
|
+
if doc:
|
|
292
|
+
existing = schema.get("description")
|
|
293
|
+
schema["description"] = (
|
|
294
|
+
f"{doc} {existing}" if existing else doc
|
|
295
|
+
)
|
|
296
|
+
return schema
|
|
297
|
+
|
|
298
|
+
def _element_to_property(
|
|
299
|
+
self,
|
|
300
|
+
el_name: str,
|
|
301
|
+
el: Any,
|
|
302
|
+
parent_hint: str,
|
|
303
|
+
qkey: tuple[str, str] | None,
|
|
304
|
+
) -> dict | None:
|
|
305
|
+
el_type = getattr(el, "type", None)
|
|
306
|
+
if el_type is None:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
base = self._type_to_schema(el_type, hint=f"{parent_hint}_{el_name}")
|
|
310
|
+
|
|
311
|
+
qname = getattr(el, "qname", None)
|
|
312
|
+
xml_meta: dict[str, Any] = {"name": el_name}
|
|
313
|
+
if qname is not None and getattr(qname, "namespace", None):
|
|
314
|
+
xml_meta["name"] = qname.localname
|
|
315
|
+
xml_meta["namespace"] = qname.namespace
|
|
316
|
+
if getattr(el, "nillable", False) and "$ref" not in base:
|
|
317
|
+
base["nullable"] = True
|
|
318
|
+
|
|
319
|
+
doc = self._child_doc(qkey, el_name)
|
|
320
|
+
|
|
321
|
+
default = getattr(el, "default", None)
|
|
322
|
+
if default is not None and "$ref" not in base:
|
|
323
|
+
base["default"] = _coerce_default(default, base)
|
|
324
|
+
|
|
325
|
+
max_occurs = getattr(el, "max_occurs", 1)
|
|
326
|
+
is_array = max_occurs == "unbounded"
|
|
327
|
+
if not is_array:
|
|
328
|
+
try:
|
|
329
|
+
is_array = int(max_occurs) > 1
|
|
330
|
+
except (TypeError, ValueError):
|
|
331
|
+
is_array = False
|
|
332
|
+
|
|
333
|
+
if is_array:
|
|
334
|
+
items = base
|
|
335
|
+
if "$ref" not in items:
|
|
336
|
+
items = dict(items)
|
|
337
|
+
items["xml"] = dict(xml_meta)
|
|
338
|
+
arr: dict[str, Any] = {"type": "array", "items": items, "xml": xml_meta}
|
|
339
|
+
try:
|
|
340
|
+
mn = int(getattr(el, "min_occurs", 0))
|
|
341
|
+
if mn > 0:
|
|
342
|
+
arr["minItems"] = mn
|
|
343
|
+
except (TypeError, ValueError):
|
|
344
|
+
pass
|
|
345
|
+
if max_occurs != "unbounded":
|
|
346
|
+
try:
|
|
347
|
+
arr["maxItems"] = int(max_occurs)
|
|
348
|
+
except (TypeError, ValueError):
|
|
349
|
+
pass
|
|
350
|
+
if doc:
|
|
351
|
+
arr["description"] = doc
|
|
352
|
+
return arr
|
|
353
|
+
|
|
354
|
+
if "$ref" in base:
|
|
355
|
+
out: dict[str, Any] = {"allOf": [base], "xml": xml_meta}
|
|
356
|
+
if doc:
|
|
357
|
+
out["description"] = doc
|
|
358
|
+
return out
|
|
359
|
+
base["xml"] = xml_meta
|
|
360
|
+
if doc and "description" not in base:
|
|
361
|
+
base["description"] = doc
|
|
362
|
+
return base
|
|
363
|
+
|
|
364
|
+
def _simple_to_schema(self, t: Any) -> dict[str, Any]:
|
|
365
|
+
if t is None:
|
|
366
|
+
return {"type": "string"}
|
|
367
|
+
cls_names = [k.__name__ for k in type(t).__mro__]
|
|
368
|
+
if type(t).__name__ in ("AnyType", "AnySimpleType"):
|
|
369
|
+
return {}
|
|
370
|
+
|
|
371
|
+
accepted = getattr(t, "accepted_types", None) or []
|
|
372
|
+
py = None
|
|
373
|
+
for cand in accepted:
|
|
374
|
+
if isinstance(cand, type):
|
|
375
|
+
py = cand
|
|
376
|
+
break
|
|
377
|
+
schema = _py_to_json_type(py or str)
|
|
378
|
+
|
|
379
|
+
for cls in cls_names:
|
|
380
|
+
fmt = _FORMAT_BY_CLS.get(cls)
|
|
381
|
+
if fmt:
|
|
382
|
+
schema.setdefault("format", fmt)
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
tns, tname = self._type_qkey(t)
|
|
386
|
+
builtin_names = {"string", "int", "integer", "decimal", "boolean",
|
|
387
|
+
"float", "double", "dateTime", "date", "time",
|
|
388
|
+
"long", "short", "byte", "anyURI", "base64Binary"}
|
|
389
|
+
if tname and tname not in builtin_names:
|
|
390
|
+
facets = self._lookup(self.meta.facets, tns, tname)
|
|
391
|
+
if facets:
|
|
392
|
+
for k, v in facets.items():
|
|
393
|
+
schema.setdefault(k, v)
|
|
394
|
+
doc = self._lookup(self.meta.type_docs, tns, tname)
|
|
395
|
+
if doc:
|
|
396
|
+
schema.setdefault("description", doc)
|
|
397
|
+
schema["x-soap-simple-type"] = tname
|
|
398
|
+
return schema
|
spec2openapi/server.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Optional MCP runtime glue (requires the [mcp] extra: fastmcp + httpx).
|
|
2
|
+
|
|
3
|
+
This module is a *reference implementation* showing how an openapi->MCP
|
|
4
|
+
runtime consumes specs generated by spec2openapi: FastMCP.from_openapi plus
|
|
5
|
+
the SOAP bridge transport that understands the x-soap extensions.
|
|
6
|
+
|
|
7
|
+
mcp = spec2openapi.from_openapi_spec(spec2openapi.load_spec("spec.yaml"))
|
|
8
|
+
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .convert import convert_wsdl, load_spec, spec_has_soap # noqa: F401
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("spec2openapi")
|
|
18
|
+
|
|
19
|
+
BRIDGE_BASE_URL = "http://spec2openapi.bridge.local"
|
|
20
|
+
|
|
21
|
+
_MCP_HINT = (
|
|
22
|
+
"the MCP runtime requires optional dependencies; "
|
|
23
|
+
"install them with: pip install 'spec2openapi[mcp]'"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def from_openapi_spec(
|
|
28
|
+
spec: dict[str, Any],
|
|
29
|
+
*,
|
|
30
|
+
name: str | None = None,
|
|
31
|
+
options: Any | None = None,
|
|
32
|
+
validate_output: bool = False,
|
|
33
|
+
**fastmcp_kwargs: Any,
|
|
34
|
+
):
|
|
35
|
+
"""OpenAPI dict -> FastMCP server.
|
|
36
|
+
|
|
37
|
+
Specs containing x-soap operations are routed through the SOAP bridge;
|
|
38
|
+
plain REST specs get a regular httpx client (one runtime serves both).
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
import httpx
|
|
42
|
+
from fastmcp import FastMCP
|
|
43
|
+
except ImportError as exc: # pragma: no cover
|
|
44
|
+
raise ImportError(_MCP_HINT) from exc
|
|
45
|
+
|
|
46
|
+
from .bridge import BridgeOptions, SoapBridgeTransport
|
|
47
|
+
|
|
48
|
+
if spec_has_soap(spec):
|
|
49
|
+
transport = SoapBridgeTransport(spec, options)
|
|
50
|
+
client = httpx.AsyncClient(transport=transport, base_url=BRIDGE_BASE_URL)
|
|
51
|
+
else:
|
|
52
|
+
options = options or BridgeOptions.from_env()
|
|
53
|
+
base = options.endpoint or (spec.get("servers") or [{}])[0].get("url", "")
|
|
54
|
+
auth = None
|
|
55
|
+
if options.auth == "basic" and options.username is not None:
|
|
56
|
+
auth = (options.username, options.password or "")
|
|
57
|
+
client = httpx.AsyncClient(
|
|
58
|
+
base_url=base, auth=auth,
|
|
59
|
+
timeout=options.timeout, verify=options.verify,
|
|
60
|
+
headers=options.headers or {},
|
|
61
|
+
trust_env=options.trust_env,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return FastMCP.from_openapi(
|
|
65
|
+
openapi_spec=spec,
|
|
66
|
+
client=client,
|
|
67
|
+
name=name or spec.get("info", {}).get("title", "spec2openapi"),
|
|
68
|
+
validate_output=validate_output,
|
|
69
|
+
**fastmcp_kwargs,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def from_wsdl(
|
|
74
|
+
source: str,
|
|
75
|
+
*,
|
|
76
|
+
name: str | None = None,
|
|
77
|
+
options: Any | None = None,
|
|
78
|
+
validate_output: bool = False,
|
|
79
|
+
**convert_kwargs: Any,
|
|
80
|
+
):
|
|
81
|
+
"""WSDL (path/URL) -> FastMCP server in one call."""
|
|
82
|
+
spec = convert_wsdl(source, **convert_kwargs)
|
|
83
|
+
return from_openapi_spec(
|
|
84
|
+
spec, name=name, options=options, validate_output=validate_output
|
|
85
|
+
)
|