pdbe-mcp-server 1.0.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.
- pdbe_mcp_server/__init__.py +16 -0
- pdbe_mcp_server/api_tools.py +291 -0
- pdbe_mcp_server/config.yaml +8 -0
- pdbe_mcp_server/graph_tools.py +224 -0
- pdbe_mcp_server/helper.py +45 -0
- pdbe_mcp_server/py.typed +2 -0
- pdbe_mcp_server/search_tools.py +141 -0
- pdbe_mcp_server/server.py +196 -0
- pdbe_mcp_server/utils.py +172 -0
- pdbe_mcp_server-1.0.0.dist-info/METADATA +321 -0
- pdbe_mcp_server-1.0.0.dist-info/RECORD +14 -0
- pdbe_mcp_server-1.0.0.dist-info/WHEEL +4 -0
- pdbe_mcp_server-1.0.0.dist-info/entry_points.txt +2 -0
- pdbe_mcp_server-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from omegaconf import DictConfig, OmegaConf
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_config() -> DictConfig:
|
|
8
|
+
"""
|
|
9
|
+
Load and return configuration from config.yaml using OmegaConf.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
DictConfig: Configuration object from config.yaml
|
|
13
|
+
"""
|
|
14
|
+
config_path: Path = Path(__file__).parent / "config.yaml"
|
|
15
|
+
config = cast(DictConfig, OmegaConf.load(config_path))
|
|
16
|
+
return config
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
|
|
5
|
+
import mcp.types as types
|
|
6
|
+
import requests
|
|
7
|
+
from omegaconf import DictConfig
|
|
8
|
+
|
|
9
|
+
from pdbe_mcp_server import get_config
|
|
10
|
+
from pdbe_mcp_server.utils import HTTPClient
|
|
11
|
+
|
|
12
|
+
conf: DictConfig = get_config()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpenAPIToMCPGenerator:
|
|
16
|
+
def __init__(self, openapi_url: str) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize the generator with an OpenAPI JSON URL.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
openapi_url: URL to the OpenAPI JSON specification
|
|
22
|
+
"""
|
|
23
|
+
self.openapi_url: str = openapi_url
|
|
24
|
+
self.openapi_spec: dict[str, Any] = {}
|
|
25
|
+
self.base_url: str = str(conf.api.base_url)
|
|
26
|
+
self.tools: list[dict[str, Any]] = []
|
|
27
|
+
|
|
28
|
+
def load_openapi_spec(self) -> dict[str, Any]:
|
|
29
|
+
"""
|
|
30
|
+
Load the OpenAPI specification from the remote URL.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The parsed OpenAPI specification as a dictionary
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
spec = HTTPClient.get(self.openapi_url)
|
|
37
|
+
self.openapi_spec = spec
|
|
38
|
+
return spec
|
|
39
|
+
|
|
40
|
+
except requests.HTTPError as e:
|
|
41
|
+
raise Exception(
|
|
42
|
+
f"Failed to load OpenAPI spec from {self.openapi_url}: {e}"
|
|
43
|
+
) from e
|
|
44
|
+
|
|
45
|
+
def _convert_openapi_type_to_json_schema(
|
|
46
|
+
self, param_schema: dict[str, Any]
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Convert OpenAPI parameter schema to JSON Schema format.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
param_schema: OpenAPI parameter schema
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
JSON Schema compatible dictionary
|
|
56
|
+
"""
|
|
57
|
+
json_schema = {}
|
|
58
|
+
|
|
59
|
+
# Map OpenAPI types to JSON Schema types
|
|
60
|
+
if "type" in param_schema:
|
|
61
|
+
json_schema["type"] = param_schema["type"]
|
|
62
|
+
|
|
63
|
+
if "description" in param_schema:
|
|
64
|
+
json_schema["description"] = param_schema["description"]
|
|
65
|
+
|
|
66
|
+
if "enum" in param_schema:
|
|
67
|
+
json_schema["enum"] = param_schema["enum"]
|
|
68
|
+
|
|
69
|
+
if "default" in param_schema:
|
|
70
|
+
json_schema["default"] = param_schema["default"]
|
|
71
|
+
|
|
72
|
+
if "format" in param_schema:
|
|
73
|
+
json_schema["format"] = param_schema["format"]
|
|
74
|
+
|
|
75
|
+
# Handle array types
|
|
76
|
+
if param_schema.get("type") == "array" and "items" in param_schema:
|
|
77
|
+
json_schema["items"] = self._convert_openapi_type_to_json_schema(
|
|
78
|
+
param_schema["items"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return json_schema
|
|
82
|
+
|
|
83
|
+
def _extract_parameters(
|
|
84
|
+
self, path_params: list[dict[str, Any]], query_params: list[dict[str, Any]]
|
|
85
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
86
|
+
"""
|
|
87
|
+
Extract and convert parameters to JSON Schema format.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
path_params: List of path parameters
|
|
91
|
+
query_params: List of query parameters
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
JSON Schema properties and required fields
|
|
95
|
+
"""
|
|
96
|
+
properties = {}
|
|
97
|
+
required = []
|
|
98
|
+
|
|
99
|
+
# Process path parameters
|
|
100
|
+
for param in path_params:
|
|
101
|
+
param_name = param["name"]
|
|
102
|
+
properties[param_name] = self._convert_openapi_type_to_json_schema(
|
|
103
|
+
param.get("schema", {})
|
|
104
|
+
)
|
|
105
|
+
schema = param.get("schema", {})
|
|
106
|
+
description = schema.get("description", "")
|
|
107
|
+
for key in schema:
|
|
108
|
+
description += f"\n{key}: {schema[key]}"
|
|
109
|
+
|
|
110
|
+
properties[param_name]["description"] = description
|
|
111
|
+
if param.get("required", False):
|
|
112
|
+
required.append(param_name)
|
|
113
|
+
|
|
114
|
+
# Process query parameters
|
|
115
|
+
for param in query_params:
|
|
116
|
+
param_name = param["name"]
|
|
117
|
+
properties[param_name] = self._convert_openapi_type_to_json_schema(
|
|
118
|
+
param.get("schema", {})
|
|
119
|
+
)
|
|
120
|
+
if param.get("description"):
|
|
121
|
+
properties[param_name]["description"] = param["description"]
|
|
122
|
+
if param.get("required", False):
|
|
123
|
+
required.append(param_name)
|
|
124
|
+
|
|
125
|
+
return properties, required
|
|
126
|
+
|
|
127
|
+
def list_tools(self) -> list[types.Tool]:
|
|
128
|
+
"""
|
|
129
|
+
Generate MCP tools from the OpenAPI specification.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of MCP Tool objects
|
|
133
|
+
"""
|
|
134
|
+
if not self.openapi_spec:
|
|
135
|
+
self.load_openapi_spec()
|
|
136
|
+
|
|
137
|
+
tools = []
|
|
138
|
+
paths = self.openapi_spec.get("paths", {})
|
|
139
|
+
|
|
140
|
+
for path, path_item in paths.items():
|
|
141
|
+
for method, operation in path_item.items():
|
|
142
|
+
# Skip POST methods and non-HTTP methods
|
|
143
|
+
if method.upper() == "POST" or method.startswith("x-"):
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# skip APIs which are not enabled for MCP
|
|
147
|
+
if not operation.get("enableMCP") or not operation["enableMCP"]:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Generate tool name from operationId or path/method
|
|
151
|
+
if "operationId" in operation:
|
|
152
|
+
tool_name = operation["operationId"]
|
|
153
|
+
else:
|
|
154
|
+
# Create tool name from path and method
|
|
155
|
+
clean_path = (
|
|
156
|
+
path.replace("/", "_")
|
|
157
|
+
.replace("{", "")
|
|
158
|
+
.replace("}", "")
|
|
159
|
+
.strip("_")
|
|
160
|
+
)
|
|
161
|
+
tool_name = f"{method}_{clean_path}".replace("__", "_")
|
|
162
|
+
|
|
163
|
+
# trim tool name to at most 60 characters
|
|
164
|
+
tool_name = tool_name[:60]
|
|
165
|
+
|
|
166
|
+
# Get description
|
|
167
|
+
description = operation.get("description", operation.get("summary"))
|
|
168
|
+
|
|
169
|
+
# Extract parameters
|
|
170
|
+
parameters = operation.get("parameters", [])
|
|
171
|
+
path_params = [p for p in parameters if p.get("in") == "path"]
|
|
172
|
+
query_params = [p for p in parameters if p.get("in") == "query"]
|
|
173
|
+
|
|
174
|
+
properties, required = self._extract_parameters(
|
|
175
|
+
path_params, query_params
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Create input schema
|
|
179
|
+
input_schema = {
|
|
180
|
+
"type": "object",
|
|
181
|
+
"properties": properties,
|
|
182
|
+
"additionalProperties": False,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if required:
|
|
186
|
+
input_schema["required"] = required
|
|
187
|
+
|
|
188
|
+
# Create tool
|
|
189
|
+
tool = types.Tool(
|
|
190
|
+
name=tool_name,
|
|
191
|
+
description=description,
|
|
192
|
+
inputSchema=input_schema,
|
|
193
|
+
annotations=types.ToolAnnotations(
|
|
194
|
+
title=description,
|
|
195
|
+
destructiveHint=False,
|
|
196
|
+
readOnlyHint=True,
|
|
197
|
+
idempotentHint=False,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
tools.append(tool)
|
|
201
|
+
|
|
202
|
+
# Store tool metadata for call_tool function
|
|
203
|
+
self.tools.append(
|
|
204
|
+
{
|
|
205
|
+
"name": tool_name,
|
|
206
|
+
"method": method.upper(),
|
|
207
|
+
"path": path,
|
|
208
|
+
"path_params": [p["name"] for p in path_params],
|
|
209
|
+
"query_params": [p["name"] for p in query_params],
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return tools
|
|
214
|
+
|
|
215
|
+
def call_tool(
|
|
216
|
+
self, name: str, arguments: dict[str, Any]
|
|
217
|
+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
|
218
|
+
"""
|
|
219
|
+
Execute a tool call by making the appropriate API request.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
name: Tool name to execute
|
|
223
|
+
arguments: Arguments for the tool
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
List of TextContent with the API response
|
|
227
|
+
"""
|
|
228
|
+
# Find the tool metadata
|
|
229
|
+
tool_info = None
|
|
230
|
+
for tool in self.tools:
|
|
231
|
+
if tool["name"] == name:
|
|
232
|
+
tool_info = tool
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
if not tool_info:
|
|
236
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
237
|
+
|
|
238
|
+
# Build the URL
|
|
239
|
+
url_path = tool_info["path"]
|
|
240
|
+
|
|
241
|
+
# Replace path parameters
|
|
242
|
+
for param_name in tool_info["path_params"]:
|
|
243
|
+
if param_name not in arguments:
|
|
244
|
+
raise ValueError(f"Missing required path parameter: {param_name}")
|
|
245
|
+
url_path = url_path.replace(f"{{{param_name}}}", str(arguments[param_name]))
|
|
246
|
+
|
|
247
|
+
# Build full URL
|
|
248
|
+
full_url = urljoin(self.base_url, url_path.lstrip("/"))
|
|
249
|
+
|
|
250
|
+
# Build path parameters
|
|
251
|
+
path_params = {}
|
|
252
|
+
for param_name in tool_info["path_params"]:
|
|
253
|
+
if param_name in arguments:
|
|
254
|
+
path_params[param_name] = arguments[param_name]
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
if tool_info["method"] == "GET":
|
|
258
|
+
data = HTTPClient.get(full_url)
|
|
259
|
+
elif tool_info["method"] == "POST":
|
|
260
|
+
data = HTTPClient.post(full_url)
|
|
261
|
+
else:
|
|
262
|
+
# unsupported method, raise an error
|
|
263
|
+
raise ValueError(f"Unsupported method: {tool_info['method']}")
|
|
264
|
+
|
|
265
|
+
result_text = json.dumps(data, indent=2)
|
|
266
|
+
return [types.TextContent(type="text", text=result_text)]
|
|
267
|
+
|
|
268
|
+
except requests.RequestException as e:
|
|
269
|
+
error_message = f"API request failed: {e}"
|
|
270
|
+
if hasattr(e, "response") and e.response is not None:
|
|
271
|
+
error_message += f"\nStatus Code: {e.response.status_code}"
|
|
272
|
+
error_message += f"\nResponse: {e.response.text}"
|
|
273
|
+
|
|
274
|
+
return [types.TextContent(type="text", text=error_message)]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def create_mcp_tools_from_openapi(
|
|
278
|
+
openapi_url: str,
|
|
279
|
+
) -> tuple[OpenAPIToMCPGenerator, list[types.Tool]]:
|
|
280
|
+
"""
|
|
281
|
+
Main function to create MCP tools from an OpenAPI specification.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
openapi_url: URL to the OpenAPI JSON specification
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Tuple of (generator_instance, list_of_tools)
|
|
288
|
+
"""
|
|
289
|
+
generator = OpenAPIToMCPGenerator(openapi_url)
|
|
290
|
+
tools = generator.list_tools()
|
|
291
|
+
return generator, tools
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
api:
|
|
2
|
+
openapi_url: "https://www.ebi.ac.uk/pdbe/api/v2/openapi.json"
|
|
3
|
+
base_url: "https://www.ebi.ac.uk/pdbe/api/v2/"
|
|
4
|
+
graph:
|
|
5
|
+
schema_url: "https://www.ebi.ac.uk/pdbe/static/files/graph_schema.json"
|
|
6
|
+
search:
|
|
7
|
+
schema_url: "https://www.ebi.ac.uk/pdbe/static/files/search_schema.json"
|
|
8
|
+
search_api: "https://www.ebi.ac.uk/pdbe/search/pdb/select?wt=json"
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import mcp.types as types
|
|
4
|
+
from omegaconf import DictConfig
|
|
5
|
+
|
|
6
|
+
from pdbe_mcp_server import get_config
|
|
7
|
+
from pdbe_mcp_server.utils import HTMLStripper, HTTPClient
|
|
8
|
+
|
|
9
|
+
conf: DictConfig = get_config()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GraphTools:
|
|
13
|
+
"""
|
|
14
|
+
A class to handle PDBe graph-related operations.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initialize the GraphTools object, load the graph schema, and prepare node and edge lists.
|
|
20
|
+
"""
|
|
21
|
+
self.graph_schema: dict[str, Any] = self._get_graph_schema()
|
|
22
|
+
self.node_dict: dict[Any, str] = {}
|
|
23
|
+
self.nodes: list[dict[str, Any]] = self.get_nodes()
|
|
24
|
+
self.edges: list[dict[str, Any]] = self.get_edges()
|
|
25
|
+
|
|
26
|
+
def get_pdbe_graph_nodes_tool(self) -> types.Tool:
|
|
27
|
+
return types.Tool(
|
|
28
|
+
name="pdbe_graph_nodes",
|
|
29
|
+
description="""
|
|
30
|
+
Retrieves metadata about all node types (also known as "labels") defined in the PDBe (PDBe-KB) graph database schema.
|
|
31
|
+
This tool can be used to understand the different types of entities represented in the PDBe graph database, along with
|
|
32
|
+
their properties and descriptions and then can be used to explore the graph more effectively by writing Cypher queries.
|
|
33
|
+
This tool returns detailed information about each node label in the graph database. For every node label, it includes:
|
|
34
|
+
- The label name (e.g., 'ValAngleOutlier', 'Antibody', 'Atom')
|
|
35
|
+
- A human-readable description of the node type
|
|
36
|
+
- A list of properties/parameters associated with this node type
|
|
37
|
+
- For each property: the name and a brief description
|
|
38
|
+
|
|
39
|
+
Expected Output Format (text):
|
|
40
|
+
Label: ValAngleOutlier
|
|
41
|
+
Description: Bond angle outliers based on wwPDB validation data.
|
|
42
|
+
Properties:
|
|
43
|
+
- ATOM0/1/2/3: Names of atoms involved in the angle which is an outlier.
|
|
44
|
+
- MEAN: The ideal value of the bond angle.
|
|
45
|
+
- OBS: The observed value of the bond angle.
|
|
46
|
+
|
|
47
|
+
(Additional node labels follow the same format...)
|
|
48
|
+
""",
|
|
49
|
+
inputSchema={
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {},
|
|
52
|
+
"additionalProperties": False,
|
|
53
|
+
},
|
|
54
|
+
annotations=types.ToolAnnotations(
|
|
55
|
+
title="Get PDBe Graph Nodes",
|
|
56
|
+
destructiveHint=False,
|
|
57
|
+
readOnlyHint=True,
|
|
58
|
+
idempotentHint=True,
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def get_pdbe_graph_edges_tool(self) -> types.Tool:
|
|
63
|
+
return types.Tool(
|
|
64
|
+
name="pdbe_graph_edges",
|
|
65
|
+
description="""
|
|
66
|
+
Retrieves metadata about all relationship types (edges) defined in the PDBe (PDBe-KB) graph database schema.
|
|
67
|
+
This tool can be used to understand the different types of relationships represented in the PDBe graph database, along with
|
|
68
|
+
their start and end nodes, properties and descriptions and then can be used to explore the graph more effectively by writing Cypher queries.
|
|
69
|
+
This tool returns detailed information about each relationship (edge) in the graph. For every relationship type, it includes:
|
|
70
|
+
- The relationship label (e.g., 'HAS_OUTLIER', 'CONNECTS_TO')
|
|
71
|
+
- A human-readable description of the relationship
|
|
72
|
+
- The 'from' node label and 'to' node label, defining the direction and connectivity
|
|
73
|
+
- A list of properties associated with the relationship
|
|
74
|
+
- For each property: the name and a brief description
|
|
75
|
+
|
|
76
|
+
Expected Output Format (text):
|
|
77
|
+
Label: HAS_OUTLIER
|
|
78
|
+
Description: Indicates a structure has validation outliers
|
|
79
|
+
From: Structure
|
|
80
|
+
To: ValAngleOutlier
|
|
81
|
+
Properties:
|
|
82
|
+
- since: The date when the outlier was detected
|
|
83
|
+
- severity: The severity level of the outlier
|
|
84
|
+
|
|
85
|
+
(Additional relationship types follow the same format...)
|
|
86
|
+
""",
|
|
87
|
+
inputSchema={
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {},
|
|
90
|
+
"additionalProperties": False,
|
|
91
|
+
},
|
|
92
|
+
annotations=types.ToolAnnotations(
|
|
93
|
+
title="Get PDBe Graph Edges",
|
|
94
|
+
destructiveHint=False,
|
|
95
|
+
readOnlyHint=True,
|
|
96
|
+
idempotentHint=True,
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _get_graph_schema(self) -> dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
Retrieve the PDBe graph schema from the remote server and return it as a dictionary.
|
|
103
|
+
"""
|
|
104
|
+
return HTTPClient.get(str(conf.graph.schema_url))
|
|
105
|
+
|
|
106
|
+
def get_nodes(self) -> list[dict[str, Any]]:
|
|
107
|
+
"""
|
|
108
|
+
Get the nodes from the graph schema, clean up their descriptions and titles, and store them in a list.
|
|
109
|
+
Also populates the node_dict for quick label lookup.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of node dictionaries.
|
|
113
|
+
"""
|
|
114
|
+
nodes = []
|
|
115
|
+
for node in self.graph_schema.get("nodes", []):
|
|
116
|
+
# clean up the node data
|
|
117
|
+
node["description"] = HTMLStripper.strip_tags(node.get("description", ""))
|
|
118
|
+
node["title"] = HTMLStripper.strip_tags(node.get("title", ""))
|
|
119
|
+
for prop in node.get("properties", []):
|
|
120
|
+
prop["value"] = HTMLStripper.strip_tags(prop.get("value", ""))
|
|
121
|
+
nodes.append(node)
|
|
122
|
+
|
|
123
|
+
# store the node label in a dictionary for quick access
|
|
124
|
+
self.node_dict[node.get("id")] = node["label"]
|
|
125
|
+
|
|
126
|
+
return nodes
|
|
127
|
+
|
|
128
|
+
def get_edges(self) -> list[dict[str, Any]]:
|
|
129
|
+
"""
|
|
130
|
+
Get the edges from the graph schema, clean up their descriptions and titles, and store them in a list.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of edge dictionaries.
|
|
134
|
+
"""
|
|
135
|
+
edges = []
|
|
136
|
+
for edge in self.graph_schema.get("edges", []):
|
|
137
|
+
# clean up the edge data
|
|
138
|
+
edge["description"] = HTMLStripper.strip_tags(edge.get("description", ""))
|
|
139
|
+
edge["title"] = HTMLStripper.strip_tags(edge.get("title", ""))
|
|
140
|
+
for prop in edge.get("properties", []):
|
|
141
|
+
prop["value"] = HTMLStripper.strip_tags(prop.get("value", ""))
|
|
142
|
+
edges.append(edge)
|
|
143
|
+
|
|
144
|
+
return edges
|
|
145
|
+
|
|
146
|
+
def format_nodes(self) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Format the nodes as a string for LLM or human-readable output.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
A formatted string listing all nodes and their properties.
|
|
152
|
+
"""
|
|
153
|
+
return "\n\n".join(
|
|
154
|
+
f"Label: {node['label']}\nDescription: {node.get('description', '')}\n"
|
|
155
|
+
+ (
|
|
156
|
+
"Properties:\n"
|
|
157
|
+
+ "\n".join(
|
|
158
|
+
f" - {prop.get('name', '')}: {prop.get('value', '')}"
|
|
159
|
+
for prop in node.get("properties", [])
|
|
160
|
+
)
|
|
161
|
+
if node.get("properties")
|
|
162
|
+
else "Properties: None"
|
|
163
|
+
)
|
|
164
|
+
for node in self.nodes
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def format_edges(self) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Format the edges as a string for LLM or human-readable output.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A formatted string listing all edges and their properties.
|
|
173
|
+
"""
|
|
174
|
+
return "\n\n".join(
|
|
175
|
+
f"Label: {edge['label']}\n"
|
|
176
|
+
f"Description: {edge.get('description', '')}\n"
|
|
177
|
+
f"From: {self.node_dict.get(edge.get('from'), edge.get('from'))}\n"
|
|
178
|
+
f"To: {self.node_dict.get(edge.get('to'), edge.get('to'))}\n"
|
|
179
|
+
+ (
|
|
180
|
+
"Properties:\n"
|
|
181
|
+
+ "\n".join(
|
|
182
|
+
f" - {prop.get('name', '')}: {prop.get('value', '')}"
|
|
183
|
+
for prop in edge.get("properties", [])
|
|
184
|
+
)
|
|
185
|
+
if edge.get("properties")
|
|
186
|
+
else "Properties: None"
|
|
187
|
+
)
|
|
188
|
+
for edge in self.edges
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def get_node_by_label(self, node_label: str) -> str | None:
|
|
192
|
+
"""
|
|
193
|
+
Get a formatted string for a node by its label.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
node_label: The label of the node to search for.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
A formatted string with node information, or None if not found.
|
|
200
|
+
"""
|
|
201
|
+
for node in self.nodes:
|
|
202
|
+
if node.get("label") == node_label:
|
|
203
|
+
# format the node
|
|
204
|
+
return f"""
|
|
205
|
+
Label: {node_label}
|
|
206
|
+
Description: {node.get("description")}
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def get_edge_by_label(self, edge_label: str) -> dict[str, Any] | None:
|
|
212
|
+
"""
|
|
213
|
+
Get an edge dictionary by its label.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
edge_label: The label of the edge to search for.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The edge dictionary if found, otherwise None.
|
|
220
|
+
"""
|
|
221
|
+
for edge in self.edges:
|
|
222
|
+
if edge.get("label") == edge_label:
|
|
223
|
+
return edge
|
|
224
|
+
return None
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_graph_info(graph_data: dict[str, Any]) -> str:
|
|
5
|
+
output = []
|
|
6
|
+
|
|
7
|
+
# Format Nodes
|
|
8
|
+
output.append("Nodes\n" + "=" * 5 + "\n")
|
|
9
|
+
|
|
10
|
+
for node in graph_data.get("nodes", []):
|
|
11
|
+
output.append(f"Label: {node['label']}")
|
|
12
|
+
output.append(f"Description: {node['description']}")
|
|
13
|
+
output.append("Properties:")
|
|
14
|
+
|
|
15
|
+
if node.get("properties"):
|
|
16
|
+
for prop in node["properties"]:
|
|
17
|
+
name = prop.get("name", "<No name>")
|
|
18
|
+
value = prop.get("value", "<No description>")
|
|
19
|
+
output.append(f" - {name}: {value}")
|
|
20
|
+
else:
|
|
21
|
+
output.append(" None")
|
|
22
|
+
|
|
23
|
+
output.append("") # Blank line between nodes
|
|
24
|
+
|
|
25
|
+
# Format Edges (Relationships)
|
|
26
|
+
output.append("Relationships\n" + "=" * 12 + "\n")
|
|
27
|
+
|
|
28
|
+
for edge in graph_data.get("edges", []):
|
|
29
|
+
output.append(f"Label: {edge['label']}")
|
|
30
|
+
output.append(f"Description: {edge['description']}")
|
|
31
|
+
output.append(f"From Node ID: {edge['from']}")
|
|
32
|
+
output.append(f"To Node ID: {edge['to']}")
|
|
33
|
+
output.append("Properties:")
|
|
34
|
+
|
|
35
|
+
if edge.get("properties"):
|
|
36
|
+
for prop in edge["properties"]:
|
|
37
|
+
name = prop.get("name", "<No name>")
|
|
38
|
+
value = prop.get("value", "<No description>")
|
|
39
|
+
output.append(f" - {name}: {value}")
|
|
40
|
+
else:
|
|
41
|
+
output.append(" None")
|
|
42
|
+
|
|
43
|
+
output.append("") # Blank line between edges
|
|
44
|
+
|
|
45
|
+
return "\n".join(output)
|
pdbe_mcp_server/py.typed
ADDED