swiftmcp 0.0.1__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.
@@ -0,0 +1,537 @@
1
+ import re
2
+ import uuid
3
+ from typing import Any, Dict, List, Tuple, Optional
4
+ from json import dumps as json_dumps
5
+ from json import loads as json_loads
6
+ from json.decoder import JSONDecodeError
7
+
8
+ import json
9
+ from requests import get
10
+ from yaml import YAMLError, safe_load
11
+
12
+ from .tool_entities import MultilingualText, ToolParameter
13
+ from .tool_bundle import ApiToolBundle
14
+
15
+
16
+ def parser_tool(json_data: dict) -> None:
17
+ """Simple tool parser example"""
18
+ base_url = json_data["servers"][0]["url"]
19
+ route_path = list(json_data["paths"].keys())[0]
20
+ url = base_url + route_path
21
+ tool_description = json_data["paths"][route_path]["post"]["description"]
22
+ # Placeholder for future implementation
23
+ pass
24
+
25
+
26
+ class ApiBasedToolSchemaParser:
27
+ """Parser for API-based tool schemas"""
28
+
29
+ @staticmethod
30
+ def parse_openapi_to_tool_bundle(openapi: dict, extra_info: Optional[dict] = None,
31
+ warning: Optional[dict] = None) -> List[ApiToolBundle]:
32
+ """
33
+ Parse OpenAPI specification to tool bundles.
34
+
35
+ Args:
36
+ openapi: OpenAPI specification dictionary
37
+ extra_info: Additional information to include
38
+ warning: Warning messages dictionary
39
+
40
+ Returns:
41
+ List of ApiToolBundle objects
42
+ """
43
+ warning = warning or {}
44
+ extra_info = extra_info or {}
45
+
46
+ # Set description to extra_info
47
+ extra_info['description'] = openapi['info'].get('description', '')
48
+
49
+ if len(openapi['servers']) == 0:
50
+ raise ValueError('No server found in the OpenAPI specification.')
51
+
52
+ server_url = openapi['servers'][0]['url']
53
+
54
+ # List all interfaces
55
+ interfaces = []
56
+ for path, path_item in openapi['paths'].items():
57
+ methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']
58
+ for method in methods:
59
+ if method in path_item:
60
+ interfaces.append({
61
+ 'path': path,
62
+ 'method': method,
63
+ 'operation': path_item[method],
64
+ })
65
+
66
+ # Process all operations
67
+ bundles = []
68
+ for interface in interfaces:
69
+ # Convert parameters
70
+ parameters = []
71
+ if 'parameters' in interface['operation']:
72
+ for parameter in interface['operation']['parameters']:
73
+ tool_parameter = ToolParameter(
74
+ name=parameter['name'],
75
+ label=MultilingualText(
76
+ en_US=parameter['name'],
77
+ zh_Hans=parameter['name']
78
+ ),
79
+ human_description=MultilingualText(
80
+ en_US=parameter.get('description', ''),
81
+ zh_Hans=parameter.get('description', '')
82
+ ),
83
+ type=ToolParameter.ToolParameterType.STRING,
84
+ required=parameter.get('required', False),
85
+ form=ToolParameter.ToolParameterForm.LLM,
86
+ llm_description=parameter.get('description'),
87
+ default=parameter['schema']['default'] if 'schema' in parameter and 'default' in parameter['schema'] else None,
88
+ )
89
+
90
+ # Check if there is a type
91
+ param_type = ApiBasedToolSchemaParser._get_tool_parameter_type(parameter)
92
+ if param_type:
93
+ tool_parameter.type = param_type
94
+
95
+ parameters.append(tool_parameter)
96
+
97
+ # Handle request body
98
+ if 'requestBody' in interface['operation']:
99
+ parameters.extend(ApiBasedToolSchemaParser._parse_request_body(interface))
100
+
101
+ # Handle responses (returns)
102
+ returns = ApiBasedToolSchemaParser._parse_responses(interface, openapi)
103
+
104
+ # Check if parameters are duplicated
105
+ parameters_count = {}
106
+ for parameter in parameters:
107
+ parameters_count[parameter.name] = parameters_count.get(parameter.name, 0) + 1
108
+
109
+ for name, count in parameters_count.items():
110
+ if count > 1:
111
+ warning['duplicated_parameter'] = f'Parameter {name} is duplicated.'
112
+
113
+ # Ensure operation ID exists
114
+ if 'operationId' not in interface['operation']:
115
+ interface['operation']['operationId'] = ApiBasedToolSchemaParser._generate_operation_id(
116
+ interface['path'], interface['method']
117
+ )
118
+
119
+ bundles.append(ApiToolBundle(
120
+ server_url=server_url + interface['path'],
121
+ method=interface['method'],
122
+ summary=interface['operation'].get('description') or interface['operation'].get('summary'),
123
+ operation_id=interface['operation']['operationId'],
124
+ parameters=parameters,
125
+ returns=returns,
126
+ author='',
127
+ icon=None,
128
+ openapi=interface['operation'],
129
+ ))
130
+
131
+ return bundles
132
+
133
+ @staticmethod
134
+ def _parse_request_body(interface: dict) -> List[ToolParameter]:
135
+ """Parse request body parameters"""
136
+ parameters = []
137
+ request_body = interface['operation']['requestBody']
138
+
139
+ if 'content' not in request_body:
140
+ return parameters
141
+
142
+ for content_type, content in request_body['content'].items():
143
+ # Handle schema references
144
+ if 'schema' in content:
145
+ if '$ref' in content['schema']:
146
+ # Resolve reference
147
+ root = ApiBasedToolSchemaParser._resolve_reference(
148
+ content['schema']['$ref'], interface['operation']
149
+ )
150
+ interface['operation']['requestBody']['content'][content_type]['schema'] = root
151
+
152
+ # Parse body parameters
153
+ if 'schema' in interface['operation']['requestBody']['content'][content_type]:
154
+ body_schema = interface['operation']['requestBody']['content'][content_type]['schema']
155
+ required = body_schema.get('required', [])
156
+ properties = body_schema.get('properties', {})
157
+
158
+ for name, property_def in properties.items():
159
+ # Determine parameter type
160
+ type_flag = ApiBasedToolSchemaParser._get_property_type_flag(property_def.get('type'))
161
+
162
+ tool_param = ToolParameter(
163
+ name=name,
164
+ label=MultilingualText(en_US=name, zh_Hans=name),
165
+ human_description=MultilingualText(
166
+ en_US=property_def.get('description', ''),
167
+ zh_Hans=property_def.get('description', '')
168
+ ),
169
+ type=type_flag,
170
+ required=name in required,
171
+ form=ToolParameter.ToolParameterForm.LLM,
172
+ llm_description=property_def.get('description', ''),
173
+ default=property_def.get('default', None),
174
+ )
175
+
176
+ # Check if there is a type
177
+ typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property_def)
178
+ if typ:
179
+ tool_param.type = typ
180
+ parameters.append(tool_param)
181
+
182
+ return parameters
183
+
184
+ @staticmethod
185
+ def _parse_responses(interface: dict, openapi: dict) -> List[ToolParameter]:
186
+ """Parse response parameters"""
187
+ returns = []
188
+ if 'responses' not in interface['operation']:
189
+ return returns
190
+
191
+ responses = interface['operation']['responses']
192
+ if '200' not in responses:
193
+ return returns
194
+
195
+ if 'content' not in responses['200']:
196
+ return returns
197
+
198
+ for content_type, content in responses['200']['content'].items():
199
+ # Handle schema references
200
+ if 'schema' in content:
201
+ if '$ref' in content['schema']:
202
+ # Resolve reference
203
+ root = ApiBasedToolSchemaParser._resolve_reference(
204
+ content['schema']['$ref'], openapi
205
+ )
206
+ # Note: There was a typo in the original code ('responsed' instead of 'responses')
207
+ interface['operation']['responses']['200']['content'][content_type]['schema'] = root
208
+
209
+ # Parse response schema
210
+ if 'schema' in interface['operation']['responses']['200']['content'][content_type]:
211
+ body_schema = interface['operation']['responses']['200']['content'][content_type]['schema']
212
+ required = body_schema.get('required', [])
213
+ properties = body_schema.get('properties', {})
214
+
215
+ for name, property_def in properties.items():
216
+ tool_param = ToolParameter(
217
+ name=name,
218
+ label=MultilingualText(en_US=name, zh_Hans=name),
219
+ human_description=MultilingualText(
220
+ en_US=property_def.get('description', ''),
221
+ zh_Hans=property_def.get('description', '')
222
+ ),
223
+ type=ToolParameter.ToolParameterType.STRING,
224
+ required=name in required,
225
+ form=ToolParameter.ToolParameterForm.LLM,
226
+ llm_description=property_def.get('description', ''),
227
+ default=property_def.get('default', None),
228
+ )
229
+
230
+ # Check if there is a type
231
+ typ = ApiBasedToolSchemaParser._get_tool_parameter_type(property_def)
232
+ if typ:
233
+ tool_param.type = typ
234
+ returns.append(tool_param)
235
+
236
+ return returns
237
+
238
+ @staticmethod
239
+ def _resolve_reference(ref: str, root: dict) -> dict:
240
+ """Resolve a JSON reference"""
241
+ reference = ref.split('/')[1:]
242
+ for ref_part in reference:
243
+ root = root[ref_part]
244
+ return root
245
+
246
+ @staticmethod
247
+ def _get_property_type_flag(type_str: Optional[str]) -> ToolParameter.ToolParameterType:
248
+ """Get ToolParameterType from string"""
249
+ type_mapping = {
250
+ 'int': ToolParameter.ToolParameterType.INT,
251
+ 'bool': ToolParameter.ToolParameterType.BOOL,
252
+ 'number': ToolParameter.ToolParameterType.NUMBER,
253
+ 'float': ToolParameter.ToolParameterType.FLOAT,
254
+ 'string': ToolParameter.ToolParameterType.STRING,
255
+ }
256
+ return type_mapping.get(type_str, ToolParameter.ToolParameterType.STRING)
257
+
258
+ @staticmethod
259
+ def _generate_operation_id(path: str, method: str) -> str:
260
+ """Generate an operation ID from path and method"""
261
+ # Remove leading slash
262
+ if path.startswith('/'):
263
+ path = path[1:]
264
+ # Remove special characters
265
+ path = re.sub(r'[^a-zA-Z0-9_-]', '', path)
266
+ # Use UUID if path is empty
267
+ if not path:
268
+ path = str(uuid.uuid4())
269
+ return f'{path}_{method}'
270
+
271
+ @staticmethod
272
+ def _get_tool_parameter_type(parameter: dict) -> Optional[ToolParameter.ToolParameterType]:
273
+ """Get tool parameter type from parameter definition"""
274
+ parameter = parameter or {}
275
+ typ = None
276
+
277
+ if 'type' in parameter:
278
+ typ = parameter['type']
279
+ elif 'schema' in parameter and 'type' in parameter['schema']:
280
+ typ = parameter['schema']['type']
281
+
282
+ type_mapping = {
283
+ 'integer': ToolParameter.ToolParameterType.NUMBER,
284
+ 'number': ToolParameter.ToolParameterType.NUMBER,
285
+ 'boolean': ToolParameter.ToolParameterType.BOOLEAN,
286
+ 'string': ToolParameter.ToolParameterType.STRING
287
+ }
288
+
289
+ return type_mapping.get(typ)
290
+
291
+ @staticmethod
292
+ def parse_openapi_yaml_to_tool_bundle(yaml: str, extra_info: Optional[dict] = None,
293
+ warning: Optional[dict] = None) -> List[ApiToolBundle]:
294
+ """
295
+ Parse OpenAPI YAML to tool bundle.
296
+
297
+ Args:
298
+ yaml: The YAML string
299
+ extra_info: Additional information to include
300
+ warning: Warning messages dictionary
301
+
302
+ Returns:
303
+ List of ApiToolBundle objects
304
+ """
305
+ warning = warning or {}
306
+ extra_info = extra_info or {}
307
+
308
+ openapi: dict = safe_load(yaml)
309
+ if openapi is None:
310
+ raise ValueError('Invalid OpenAPI YAML.')
311
+
312
+ return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(
313
+ openapi, extra_info=extra_info, warning=warning
314
+ )
315
+
316
+ @staticmethod
317
+ def parse_swagger_to_openapi(swagger: dict, extra_info: Optional[dict] = None,
318
+ warning: Optional[dict] = None) -> dict:
319
+ """
320
+ Parse Swagger specification to OpenAPI.
321
+
322
+ Args:
323
+ swagger: The Swagger dictionary
324
+ extra_info: Additional information to include
325
+ warning: Warning messages dictionary
326
+
327
+ Returns:
328
+ OpenAPI dictionary
329
+ """
330
+ warning = warning or {}
331
+ extra_info = extra_info or {}
332
+
333
+ # Convert swagger to openapi
334
+ info = swagger.get('info', {
335
+ 'title': 'Swagger',
336
+ 'description': 'Swagger',
337
+ 'version': '1.0.0'
338
+ })
339
+
340
+ servers = swagger.get('servers', [])
341
+ if len(servers) == 0:
342
+ raise ValueError('No server found in the Swagger specification.')
343
+
344
+ openapi = {
345
+ 'openapi': '3.0.0',
346
+ 'info': {
347
+ 'title': info.get('title', 'Swagger'),
348
+ 'description': info.get('description', 'Swagger'),
349
+ 'version': info.get('version', '1.0.0')
350
+ },
351
+ 'servers': swagger['servers'],
352
+ 'paths': {},
353
+ 'components': {
354
+ 'schemas': {}
355
+ }
356
+ }
357
+
358
+ # Check paths
359
+ if 'paths' not in swagger or len(swagger['paths']) == 0:
360
+ raise ValueError('No paths found in the Swagger specification.')
361
+
362
+ # Convert paths
363
+ for path, path_item in swagger['paths'].items():
364
+ openapi['paths'][path] = {}
365
+ for method, operation in path_item.items():
366
+ if 'operationId' not in operation:
367
+ raise ValueError(f'No operationId found in operation {method} {path}.')
368
+
369
+ if not operation.get('summary') and not operation.get('description'):
370
+ warning['missing_summary'] = f'No summary or description found in operation {method} {path}.'
371
+
372
+ openapi['paths'][path][method] = {
373
+ 'operationId': operation['operationId'],
374
+ 'summary': operation.get('summary', ''),
375
+ 'description': operation.get('description', ''),
376
+ 'parameters': operation.get('parameters', []),
377
+ 'responses': operation.get('responses', {}),
378
+ }
379
+
380
+ if 'requestBody' in operation:
381
+ openapi['paths'][path][method]['requestBody'] = operation['requestBody']
382
+
383
+ # Convert definitions
384
+ for name, definition in swagger.get('definitions', {}).items():
385
+ openapi['components']['schemas'][name] = definition
386
+
387
+ return openapi
388
+
389
+ @staticmethod
390
+ def parse_openai_plugin_json_to_tool_bundle(json_str: str, extra_info: Optional[dict] = None,
391
+ warning: Optional[dict] = None) -> List[ApiToolBundle]:
392
+ """
393
+ Parse OpenAI plugin JSON to tool bundle.
394
+
395
+ Args:
396
+ json_str: The JSON string
397
+ extra_info: Additional information to include
398
+ warning: Warning messages dictionary
399
+
400
+ Returns:
401
+ List of ApiToolBundle objects
402
+ """
403
+ warning = warning or {}
404
+ extra_info = extra_info or {}
405
+
406
+ try:
407
+ openai_plugin = json_loads(json_str)
408
+ api = openai_plugin['api']
409
+ api_url = api['url']
410
+ api_type = api['type']
411
+ except (KeyError, JSONDecodeError):
412
+ raise ValueError('Invalid OpenAI plugin JSON.')
413
+
414
+ if api_type != 'openapi':
415
+ raise ValueError('Only OpenAPI format is supported.')
416
+
417
+ # Get OpenAPI YAML
418
+ response = get(api_url, headers={
419
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
420
+ }, timeout=5)
421
+
422
+ if response.status_code != 200:
423
+ raise ValueError('Cannot get OpenAPI YAML from URL.')
424
+
425
+ return ApiBasedToolSchemaParser.parse_openapi_yaml_to_tool_bundle(
426
+ response.text, extra_info=extra_info, warning=warning
427
+ )
428
+
429
+ @staticmethod
430
+ def auto_parse_to_tool_bundle(content: str, extra_info: Optional[dict] = None,
431
+ warning: Optional[dict] = None) -> Tuple[List[ApiToolBundle], str]:
432
+ """
433
+ Automatically parse content to tool bundle.
434
+
435
+ Args:
436
+ content: The content to parse
437
+ extra_info: Additional information to include
438
+ warning: Warning messages dictionary
439
+
440
+ Returns:
441
+ Tuple of (tool bundles, schema type)
442
+ """
443
+ warning = warning or {}
444
+ extra_info = extra_info or {}
445
+
446
+ content = content.strip()
447
+ loaded_content = None
448
+ json_error = None
449
+ yaml_error = None
450
+
451
+ try:
452
+ loaded_content = json_loads(content)
453
+ except JSONDecodeError as e:
454
+ json_error = e
455
+
456
+ if loaded_content is None:
457
+ try:
458
+ loaded_content = safe_load(content)
459
+ except YAMLError as e:
460
+ yaml_error = e
461
+
462
+ if loaded_content is None:
463
+ raise ValueError(
464
+ f'Invalid API schema, schema is neither JSON nor YAML. '
465
+ f'JSON error: {str(json_error)}, YAML error: {str(yaml_error)}'
466
+ )
467
+
468
+ # Try parsing as OpenAPI
469
+ try:
470
+ openapi = ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(
471
+ loaded_content, extra_info=extra_info, warning=warning
472
+ )
473
+ schema_type = "openapi"
474
+ return openapi, schema_type
475
+ except Exception as e:
476
+ # Store the error for potential debugging
477
+ openapi_error = e
478
+
479
+ # Try parsing as Swagger
480
+ try:
481
+ converted_swagger = ApiBasedToolSchemaParser.parse_swagger_to_openapi(
482
+ loaded_content, extra_info=extra_info, warning=warning
483
+ )
484
+ schema_type = "swagger" # Using string instead of undefined ApiProviderSchemaType.SWAGGER.value
485
+ return ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(
486
+ converted_swagger, extra_info=extra_info, warning=warning
487
+ ), schema_type
488
+ except Exception as e:
489
+ swagger_error = e
490
+
491
+ # Try parsing as OpenAI plugin
492
+ try:
493
+ openapi_plugin = ApiBasedToolSchemaParser.parse_openai_plugin_json_to_tool_bundle(
494
+ json_dumps(loaded_content), extra_info=extra_info, warning=warning
495
+ )
496
+ schema_type = "openai_plugin" # Using string instead of undefined ApiProviderSchemaType.OPENAI_PLUGIN.value
497
+ return openapi_plugin, schema_type
498
+ except Exception as e:
499
+ openapi_plugin_error = e
500
+
501
+ raise ValueError(
502
+ f'Invalid API schema. '
503
+ f'OpenAPI error: {str(openapi_error)}, '
504
+ f'Swagger error: {str(swagger_error)}, '
505
+ f'OpenAI plugin error: {str(openapi_plugin_error)}'
506
+ )
507
+
508
+
509
+ def parser_tool_from_path(tool_path: str, file_reader=None) -> Tuple[List[ApiToolBundle], str]:
510
+ """
511
+ Parse tool from file path.
512
+
513
+ Args:
514
+ tool_path: Path to the tool file
515
+ file_reader: File reader function (defaults to file_db.read_json)
516
+
517
+ Returns:
518
+ Tuple of (tool bundles, schema type)
519
+ """
520
+ if file_reader is None:
521
+ from swiftmcp.utils.file_db import AgentFileDB
522
+ file_db = AgentFileDB(db_path="tmp/test.db")
523
+ file_reader = file_db
524
+
525
+ content_json = file_reader.read_json(tool_path)
526
+ content = json.dumps(content_json)
527
+ return ApiBasedToolSchemaParser.auto_parse_to_tool_bundle(content)
528
+
529
+
530
+ if __name__ == "__main__":
531
+ tool_bundles, schema_type = parser_tool_from_path("data/tools/SampleTool.json")
532
+ server_url: str = tool_bundles[0].server_url
533
+ request_method: str = tool_bundles[0].method
534
+ summary: str = tool_bundles[0].summary
535
+ parameters: list = tool_bundles[0].parameters
536
+ returns: list = tool_bundles[0].returns
537
+ operation_id = tool_bundles[0].operation_id