-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_tools.py
More file actions
107 lines (92 loc) · 3.8 KB
/
Copy path_tools.py
File metadata and controls
107 lines (92 loc) · 3.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""OpenAPI 3.1 → MCP tool catalog."""
from __future__ import annotations
from typing import Any
def openapi_to_tools(
spec: dict[str, Any],
*,
include_only: set[str] | None = None,
exclude: set[str] | None = None,
) -> list[dict[str, Any]]:
"""Convert an OpenAPI 3.1 spec into a flat list of MCP tool definitions.
One tool per (path, method). Tool name is ``operationId`` when present,
otherwise ``{method}_{path}`` with non-identifier characters squashed.
``include_only`` / ``exclude`` filter by tool name.
"""
tools: list[dict[str, Any]] = []
paths = spec.get("paths", {})
if not isinstance(paths, dict):
return tools
for path, ops in paths.items():
if not isinstance(ops, dict):
continue
for method, op in ops.items():
if method.lower() not in {"get", "post", "put", "patch", "delete", "head", "options"}:
continue
if not isinstance(op, dict):
continue
name = _tool_name(op.get("operationId"), method, path)
if include_only is not None and name not in include_only:
continue
if exclude is not None and name in exclude:
continue
description = op.get("summary") or op.get("description") or f"{method.upper()} {path}"
tools.append(
{
"name": name,
"description": description,
"inputSchema": _input_schema(op),
"_meta": {
"method": method.upper(),
"path": path,
"operation_id": op.get("operationId"),
},
}
)
return tools
def _tool_name(operation_id: Any, method: str, path: str) -> str:
if isinstance(operation_id, str) and operation_id:
return operation_id
sanitized = path.replace("/", "_").replace("{", "").replace("}", "").strip("_")
if not sanitized:
sanitized = "root"
return f"{method.lower()}_{sanitized}"
def _input_schema(op: dict[str, Any]) -> dict[str, Any]:
"""Synthesise a JSON Schema describing the tool input.
Combines path parameters, query parameters, header parameters, and the
request body (if any) into a single object schema. Path / query / header
parameter names get prefixed namespaces to avoid collisions.
"""
properties: dict[str, Any] = {}
required: list[str] = []
for param in op.get("parameters", []):
if not isinstance(param, dict):
continue
loc = param.get("in")
name = param.get("name")
# "cookie" is deliberately excluded: exposing session cookies as tool
# inputs trains agents to handle credentials as ordinary data.
if not isinstance(name, str) or loc not in {"path", "query", "header"}:
continue
schema = param.get("schema") or {"type": "string"}
key = f"{loc}.{name}"
properties[key] = dict(schema)
if "description" in param and "description" not in properties[key]:
properties[key]["description"] = param["description"]
if param.get("required") or loc == "path":
required.append(key)
body = op.get("requestBody")
if isinstance(body, dict):
content = body.get("content", {})
if isinstance(content, dict):
json_media = content.get("application/json")
if isinstance(json_media, dict):
body_schema = json_media.get("schema") or {"type": "object"}
properties["body"] = body_schema
if body.get("required"):
required.append("body")
return {
"type": "object",
"properties": properties,
"required": required,
}
__all__ = ["openapi_to_tools"]