PyStreamMCP 0.3.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.
- pystreammcp/__init__.py +24 -0
- pystreammcp/api.py +273 -0
- pystreammcp/cli.py +385 -0
- pystreammcp/core.py +567 -0
- pystreammcp/persistence.py +276 -0
- pystreammcp/routing.py +239 -0
- pystreammcp-0.3.0.dist-info/METADATA +324 -0
- pystreammcp-0.3.0.dist-info/RECORD +12 -0
- pystreammcp-0.3.0.dist-info/WHEEL +5 -0
- pystreammcp-0.3.0.dist-info/entry_points.txt +2 -0
- pystreammcp-0.3.0.dist-info/licenses/LICENSE +21 -0
- pystreammcp-0.3.0.dist-info/top_level.txt +1 -0
pystreammcp/core.py
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
"""Core metadata indexing and discovery engine for StreamMCP."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from dataclasses import dataclass, field, asdict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CapabilityClassification(Enum):
|
|
12
|
+
"""Classification of tool/capability types."""
|
|
13
|
+
|
|
14
|
+
Search = "search"
|
|
15
|
+
Read = "read"
|
|
16
|
+
Write = "write"
|
|
17
|
+
Compute = "compute"
|
|
18
|
+
Transform = "transform"
|
|
19
|
+
Aggregate = "aggregate"
|
|
20
|
+
Route = "route"
|
|
21
|
+
Filter = "filter"
|
|
22
|
+
Sort = "sort"
|
|
23
|
+
Join = "join"
|
|
24
|
+
Unknown = "unknown"
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def from_name(name: str) -> "CapabilityClassification":
|
|
28
|
+
"""Classify from tool name/description."""
|
|
29
|
+
lower = name.lower()
|
|
30
|
+
classifications = {
|
|
31
|
+
("search", "find", "query", "lookup"): CapabilityClassification.Search,
|
|
32
|
+
("read", "get", "fetch", "retrieve"): CapabilityClassification.Read,
|
|
33
|
+
("write", "create", "insert", "post"): CapabilityClassification.Write,
|
|
34
|
+
("compute", "calculate", "process"): CapabilityClassification.Compute,
|
|
35
|
+
("transform", "convert", "map"): CapabilityClassification.Transform,
|
|
36
|
+
("aggregate", "summarize", "group"): CapabilityClassification.Aggregate,
|
|
37
|
+
("route", "dispatch", "forward"): CapabilityClassification.Route,
|
|
38
|
+
("filter", "select", "where"): CapabilityClassification.Filter,
|
|
39
|
+
("sort", "order", "rank"): CapabilityClassification.Sort,
|
|
40
|
+
("join", "merge", "combine"): CapabilityClassification.Join,
|
|
41
|
+
}
|
|
42
|
+
for keywords, classification in classifications.items():
|
|
43
|
+
if any(kw in lower for kw in keywords):
|
|
44
|
+
return classification
|
|
45
|
+
return CapabilityClassification.Unknown
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RelationType(Enum):
|
|
49
|
+
"""Types of relationships between tools/systems."""
|
|
50
|
+
|
|
51
|
+
Produces = "produces"
|
|
52
|
+
Consumes = "consumes"
|
|
53
|
+
Transforms = "transforms"
|
|
54
|
+
Depends = "depends"
|
|
55
|
+
Triggers = "triggers"
|
|
56
|
+
Related = "related"
|
|
57
|
+
Compatible = "compatible"
|
|
58
|
+
Incompatible = "incompatible"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class JsonType(Enum):
|
|
62
|
+
"""JSON schema types."""
|
|
63
|
+
|
|
64
|
+
String = "string"
|
|
65
|
+
Integer = "integer"
|
|
66
|
+
Number = "number"
|
|
67
|
+
Boolean = "boolean"
|
|
68
|
+
Array = "array"
|
|
69
|
+
Object = "object"
|
|
70
|
+
Null = "null"
|
|
71
|
+
Unknown = "unknown"
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def from_string(s: str) -> "JsonType":
|
|
75
|
+
"""Convert string to JsonType."""
|
|
76
|
+
mapping = {
|
|
77
|
+
"string": JsonType.String,
|
|
78
|
+
"integer": JsonType.Integer,
|
|
79
|
+
"number": JsonType.Number,
|
|
80
|
+
"boolean": JsonType.Boolean,
|
|
81
|
+
"array": JsonType.Array,
|
|
82
|
+
"object": JsonType.Object,
|
|
83
|
+
"null": JsonType.Null,
|
|
84
|
+
}
|
|
85
|
+
return mapping.get(s.lower(), JsonType.Unknown)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class Metadata:
|
|
90
|
+
"""Tool metadata."""
|
|
91
|
+
|
|
92
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
93
|
+
name: str = ""
|
|
94
|
+
description: str = ""
|
|
95
|
+
server_id: str = ""
|
|
96
|
+
tool_id: str = ""
|
|
97
|
+
input_schema: dict = field(default_factory=dict)
|
|
98
|
+
output_schema: dict = field(default_factory=dict)
|
|
99
|
+
tags: List[str] = field(default_factory=list)
|
|
100
|
+
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
101
|
+
updated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class Capability:
|
|
106
|
+
"""Tool capability."""
|
|
107
|
+
|
|
108
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
109
|
+
metadata_id: str = ""
|
|
110
|
+
classification: CapabilityClassification = CapabilityClassification.Unknown
|
|
111
|
+
inputs: List[str] = field(default_factory=list)
|
|
112
|
+
outputs: List[str] = field(default_factory=list)
|
|
113
|
+
cost_estimate: float = 0.0
|
|
114
|
+
latency_estimate_ms: int = 100
|
|
115
|
+
reliability_score: float = 0.95
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class SchemaProperty:
|
|
120
|
+
"""Schema property definition."""
|
|
121
|
+
|
|
122
|
+
name: str
|
|
123
|
+
prop_type: JsonType
|
|
124
|
+
description: str = ""
|
|
125
|
+
required: bool = False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class Schema:
|
|
130
|
+
"""Tool schema."""
|
|
131
|
+
|
|
132
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
133
|
+
metadata_id: str = ""
|
|
134
|
+
base_type: JsonType = JsonType.Object
|
|
135
|
+
properties: List[SchemaProperty] = field(default_factory=list)
|
|
136
|
+
required_fields: List[str] = field(default_factory=list)
|
|
137
|
+
is_array: bool = False
|
|
138
|
+
extraction_confidence: float = 1.0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class TypeHierarchy:
|
|
143
|
+
"""Type hierarchy definition."""
|
|
144
|
+
|
|
145
|
+
parent_type: Optional[str] = None
|
|
146
|
+
child_types: List[str] = field(default_factory=list)
|
|
147
|
+
compatible_types: List[str] = field(default_factory=list)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class Relationship:
|
|
152
|
+
"""Relationship between tools/systems."""
|
|
153
|
+
|
|
154
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
155
|
+
from_id: str = ""
|
|
156
|
+
to_id: str = ""
|
|
157
|
+
relation_type: RelationType = RelationType.Related
|
|
158
|
+
confidence: float = 0.8
|
|
159
|
+
description: str = ""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class MetadataStore:
|
|
163
|
+
"""In-memory metadata storage with search."""
|
|
164
|
+
|
|
165
|
+
def __init__(self):
|
|
166
|
+
self._data: Dict[str, Metadata] = {}
|
|
167
|
+
|
|
168
|
+
def insert(self, metadata: Metadata) -> None:
|
|
169
|
+
"""Insert metadata."""
|
|
170
|
+
self._data[metadata.id] = metadata
|
|
171
|
+
|
|
172
|
+
def get(self, id: str) -> Optional[Metadata]:
|
|
173
|
+
"""Get metadata by ID."""
|
|
174
|
+
return self._data.get(id)
|
|
175
|
+
|
|
176
|
+
def get_by_server(self, server_id: str) -> List[Metadata]:
|
|
177
|
+
"""Get all metadata for a server."""
|
|
178
|
+
return [m for m in self._data.values() if m.server_id == server_id]
|
|
179
|
+
|
|
180
|
+
def get_by_tool(self, tool_id: str) -> List[Metadata]:
|
|
181
|
+
"""Get metadata for a tool."""
|
|
182
|
+
return [m for m in self._data.values() if m.tool_id == tool_id]
|
|
183
|
+
|
|
184
|
+
def search_by_name(self, query: str) -> List[Metadata]:
|
|
185
|
+
"""Search metadata by name/description."""
|
|
186
|
+
query_lower = query.lower()
|
|
187
|
+
return [
|
|
188
|
+
m
|
|
189
|
+
for m in self._data.values()
|
|
190
|
+
if query_lower in m.name.lower() or query_lower in m.description.lower()
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
def all(self) -> List[Metadata]:
|
|
194
|
+
"""Get all metadata."""
|
|
195
|
+
return list(self._data.values())
|
|
196
|
+
|
|
197
|
+
def count(self) -> int:
|
|
198
|
+
"""Get count of metadata items."""
|
|
199
|
+
return len(self._data)
|
|
200
|
+
|
|
201
|
+
def clear(self) -> None:
|
|
202
|
+
"""Clear all metadata."""
|
|
203
|
+
self._data.clear()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class CapabilityDiscovery:
|
|
207
|
+
"""Discover and classify tool capabilities."""
|
|
208
|
+
|
|
209
|
+
def __init__(self):
|
|
210
|
+
self._capabilities: Dict[str, Capability] = {}
|
|
211
|
+
|
|
212
|
+
def discover_from_metadata(self, metadata: Metadata) -> Capability:
|
|
213
|
+
"""Discover capability from metadata."""
|
|
214
|
+
classification = self._classify_by_schema(metadata.name, metadata.description)
|
|
215
|
+
inputs = self._extract_parameters(metadata.input_schema)
|
|
216
|
+
outputs = self._extract_parameters(metadata.output_schema)
|
|
217
|
+
|
|
218
|
+
capability = Capability(
|
|
219
|
+
metadata_id=metadata.id,
|
|
220
|
+
classification=classification,
|
|
221
|
+
inputs=inputs,
|
|
222
|
+
outputs=outputs,
|
|
223
|
+
)
|
|
224
|
+
self._capabilities[capability.id] = capability
|
|
225
|
+
return capability
|
|
226
|
+
|
|
227
|
+
def get_capability(self, id: str) -> Optional[Capability]:
|
|
228
|
+
"""Get capability by ID."""
|
|
229
|
+
return self._capabilities.get(id)
|
|
230
|
+
|
|
231
|
+
def get_by_classification(
|
|
232
|
+
self, classification: CapabilityClassification
|
|
233
|
+
) -> List[Capability]:
|
|
234
|
+
"""Get capabilities by classification."""
|
|
235
|
+
return [
|
|
236
|
+
c for c in self._capabilities.values() if c.classification == classification
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
def get_by_metadata(self, metadata_id: str) -> List[Capability]:
|
|
240
|
+
"""Get capabilities for metadata."""
|
|
241
|
+
return [
|
|
242
|
+
c for c in self._capabilities.values() if c.metadata_id == metadata_id
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
def all(self) -> List[Capability]:
|
|
246
|
+
"""Get all capabilities."""
|
|
247
|
+
return list(self._capabilities.values())
|
|
248
|
+
|
|
249
|
+
def count(self) -> int:
|
|
250
|
+
"""Get count of capabilities."""
|
|
251
|
+
return len(self._capabilities)
|
|
252
|
+
|
|
253
|
+
def _classify_by_schema(self, name: str, description: str) -> CapabilityClassification:
|
|
254
|
+
"""Classify by tool name and description."""
|
|
255
|
+
combined = f"{name} {description}".lower()
|
|
256
|
+
|
|
257
|
+
if "search" in combined or "find" in combined or "query" in combined:
|
|
258
|
+
return CapabilityClassification.Search
|
|
259
|
+
elif "read" in combined or "get" in combined or "fetch" in combined:
|
|
260
|
+
return CapabilityClassification.Read
|
|
261
|
+
elif "write" in combined or "create" in combined or "insert" in combined:
|
|
262
|
+
return CapabilityClassification.Write
|
|
263
|
+
elif "compute" in combined or "calculate" in combined:
|
|
264
|
+
return CapabilityClassification.Compute
|
|
265
|
+
elif "transform" in combined or "convert" in combined:
|
|
266
|
+
return CapabilityClassification.Transform
|
|
267
|
+
elif "aggregate" in combined or "summarize" in combined:
|
|
268
|
+
return CapabilityClassification.Aggregate
|
|
269
|
+
elif "filter" in combined or "select" in combined:
|
|
270
|
+
return CapabilityClassification.Filter
|
|
271
|
+
elif "sort" in combined or "order" in combined:
|
|
272
|
+
return CapabilityClassification.Sort
|
|
273
|
+
elif "join" in combined or "merge" in combined:
|
|
274
|
+
return CapabilityClassification.Join
|
|
275
|
+
return CapabilityClassification.Unknown
|
|
276
|
+
|
|
277
|
+
def _extract_parameters(self, schema: dict) -> List[str]:
|
|
278
|
+
"""Extract parameters from schema."""
|
|
279
|
+
if not isinstance(schema, dict):
|
|
280
|
+
return []
|
|
281
|
+
props = schema.get("properties", {})
|
|
282
|
+
if isinstance(props, dict):
|
|
283
|
+
return list(props.keys())
|
|
284
|
+
return []
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class SchemaExtractor:
|
|
288
|
+
"""Extract and manage schemas from tool definitions."""
|
|
289
|
+
|
|
290
|
+
def __init__(self):
|
|
291
|
+
self._schemas: Dict[str, Schema] = {}
|
|
292
|
+
self._hierarchies: Dict[str, TypeHierarchy] = {}
|
|
293
|
+
|
|
294
|
+
def extract(self, metadata_id: str, schema_json: dict) -> Schema:
|
|
295
|
+
"""Extract schema from JSON."""
|
|
296
|
+
base_type = self._infer_type(schema_json)
|
|
297
|
+
properties = self._extract_properties(schema_json)
|
|
298
|
+
required = self._extract_required_fields(schema_json)
|
|
299
|
+
|
|
300
|
+
schema = Schema(
|
|
301
|
+
metadata_id=metadata_id,
|
|
302
|
+
base_type=base_type,
|
|
303
|
+
properties=properties,
|
|
304
|
+
required_fields=required,
|
|
305
|
+
)
|
|
306
|
+
self._schemas[schema.id] = schema
|
|
307
|
+
return schema
|
|
308
|
+
|
|
309
|
+
def extract_input_output(
|
|
310
|
+
self, metadata_id: str, input_json: dict, output_json: dict
|
|
311
|
+
) -> Tuple[Schema, Schema]:
|
|
312
|
+
"""Extract input and output schemas."""
|
|
313
|
+
input_schema = self.extract(f"{metadata_id}_input", input_json)
|
|
314
|
+
output_schema = self.extract(f"{metadata_id}_output", output_json)
|
|
315
|
+
return input_schema, output_schema
|
|
316
|
+
|
|
317
|
+
def get_schema(self, id: str) -> Optional[Schema]:
|
|
318
|
+
"""Get schema by ID."""
|
|
319
|
+
return self._schemas.get(id)
|
|
320
|
+
|
|
321
|
+
def get_by_metadata(self, metadata_id: str) -> List[Schema]:
|
|
322
|
+
"""Get schemas for metadata."""
|
|
323
|
+
return [s for s in self._schemas.values() if metadata_id in s.metadata_id]
|
|
324
|
+
|
|
325
|
+
def build_hierarchy(self, type_name: str, parent: Optional[str] = None) -> None:
|
|
326
|
+
"""Build type hierarchy."""
|
|
327
|
+
hierarchy = TypeHierarchy(parent_type=parent)
|
|
328
|
+
self._hierarchies[type_name] = hierarchy
|
|
329
|
+
|
|
330
|
+
def get_hierarchy(self, type_name: str) -> Optional[TypeHierarchy]:
|
|
331
|
+
"""Get type hierarchy."""
|
|
332
|
+
return self._hierarchies.get(type_name)
|
|
333
|
+
|
|
334
|
+
def all_schemas(self) -> List[Schema]:
|
|
335
|
+
"""Get all schemas."""
|
|
336
|
+
return list(self._schemas.values())
|
|
337
|
+
|
|
338
|
+
def count(self) -> int:
|
|
339
|
+
"""Get count of schemas."""
|
|
340
|
+
return len(self._schemas)
|
|
341
|
+
|
|
342
|
+
def _infer_type(self, schema: dict) -> JsonType:
|
|
343
|
+
"""Infer JSON type from schema."""
|
|
344
|
+
if isinstance(schema, dict):
|
|
345
|
+
if "type" in schema:
|
|
346
|
+
return JsonType.from_string(schema["type"])
|
|
347
|
+
if "properties" in schema:
|
|
348
|
+
return JsonType.Object
|
|
349
|
+
if "items" in schema:
|
|
350
|
+
return JsonType.Array
|
|
351
|
+
return JsonType.Unknown
|
|
352
|
+
|
|
353
|
+
def _extract_properties(self, schema: dict) -> List[SchemaProperty]:
|
|
354
|
+
"""Extract properties from schema."""
|
|
355
|
+
if not isinstance(schema, dict):
|
|
356
|
+
return []
|
|
357
|
+
|
|
358
|
+
props_dict = schema.get("properties", {})
|
|
359
|
+
if not isinstance(props_dict, dict):
|
|
360
|
+
return []
|
|
361
|
+
|
|
362
|
+
properties = []
|
|
363
|
+
for name, prop in props_dict.items():
|
|
364
|
+
if isinstance(prop, dict):
|
|
365
|
+
prop_type = JsonType.from_string(prop.get("type", "unknown"))
|
|
366
|
+
description = prop.get("description", "")
|
|
367
|
+
properties.append(
|
|
368
|
+
SchemaProperty(name=name, prop_type=prop_type, description=description)
|
|
369
|
+
)
|
|
370
|
+
return properties
|
|
371
|
+
|
|
372
|
+
def _extract_required_fields(self, schema: dict) -> List[str]:
|
|
373
|
+
"""Extract required fields from schema."""
|
|
374
|
+
if not isinstance(schema, dict):
|
|
375
|
+
return []
|
|
376
|
+
required = schema.get("required", [])
|
|
377
|
+
return required if isinstance(required, list) else []
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class RelationshipGraph:
|
|
381
|
+
"""Build and query relationship graphs between tools/systems."""
|
|
382
|
+
|
|
383
|
+
def __init__(self):
|
|
384
|
+
self._relationships: Dict[str, Relationship] = {}
|
|
385
|
+
self._edges: Dict[str, List[str]] = {}
|
|
386
|
+
|
|
387
|
+
def add_relationship(self, relationship: Relationship) -> None:
|
|
388
|
+
"""Add relationship to graph."""
|
|
389
|
+
self._relationships[relationship.id] = relationship
|
|
390
|
+
|
|
391
|
+
if relationship.from_id not in self._edges:
|
|
392
|
+
self._edges[relationship.from_id] = []
|
|
393
|
+
self._edges[relationship.from_id].append(relationship.to_id)
|
|
394
|
+
|
|
395
|
+
in_key = f"_{relationship.to_id}"
|
|
396
|
+
if in_key not in self._edges:
|
|
397
|
+
self._edges[in_key] = []
|
|
398
|
+
self._edges[in_key].append(relationship.from_id)
|
|
399
|
+
|
|
400
|
+
def get_relationship(self, id: str) -> Optional[Relationship]:
|
|
401
|
+
"""Get relationship by ID."""
|
|
402
|
+
return self._relationships.get(id)
|
|
403
|
+
|
|
404
|
+
def get_outgoing(self, from_id: str) -> List[Relationship]:
|
|
405
|
+
"""Get outgoing relationships."""
|
|
406
|
+
if from_id not in self._edges:
|
|
407
|
+
return []
|
|
408
|
+
|
|
409
|
+
to_ids = self._edges[from_id]
|
|
410
|
+
result = []
|
|
411
|
+
for rel in self._relationships.values():
|
|
412
|
+
if rel.from_id == from_id and rel.to_id in to_ids:
|
|
413
|
+
result.append(rel)
|
|
414
|
+
return result
|
|
415
|
+
|
|
416
|
+
def get_incoming(self, to_id: str) -> List[Relationship]:
|
|
417
|
+
"""Get incoming relationships."""
|
|
418
|
+
key = f"_{to_id}"
|
|
419
|
+
if key not in self._edges:
|
|
420
|
+
return []
|
|
421
|
+
|
|
422
|
+
from_ids = self._edges[key]
|
|
423
|
+
result = []
|
|
424
|
+
for rel in self._relationships.values():
|
|
425
|
+
if rel.to_id == to_id and rel.from_id in from_ids:
|
|
426
|
+
result.append(rel)
|
|
427
|
+
return result
|
|
428
|
+
|
|
429
|
+
def get_by_type(self, relation_type: RelationType) -> List[Relationship]:
|
|
430
|
+
"""Get relationships by type."""
|
|
431
|
+
return [r for r in self._relationships.values() if r.relation_type == relation_type]
|
|
432
|
+
|
|
433
|
+
def find_path(self, from_id: str, to_id: str, max_depth: int = 5) -> Optional[List[str]]:
|
|
434
|
+
"""Find path between two nodes."""
|
|
435
|
+
visited: Set[str] = set()
|
|
436
|
+
path: List[str] = []
|
|
437
|
+
result = self._dfs(from_id, to_id, visited, path, max_depth)
|
|
438
|
+
return result if result else None
|
|
439
|
+
|
|
440
|
+
def all_relationships(self) -> List[Relationship]:
|
|
441
|
+
"""Get all relationships."""
|
|
442
|
+
return list(self._relationships.values())
|
|
443
|
+
|
|
444
|
+
def count(self) -> int:
|
|
445
|
+
"""Get count of relationships."""
|
|
446
|
+
return len(self._relationships)
|
|
447
|
+
|
|
448
|
+
def _dfs(
|
|
449
|
+
self,
|
|
450
|
+
current: str,
|
|
451
|
+
target: str,
|
|
452
|
+
visited: Set[str],
|
|
453
|
+
path: List[str],
|
|
454
|
+
depth: int,
|
|
455
|
+
) -> Optional[List[str]]:
|
|
456
|
+
"""Depth-first search for path."""
|
|
457
|
+
if depth == 0:
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
if current == target:
|
|
461
|
+
return path + [target]
|
|
462
|
+
|
|
463
|
+
if current in visited:
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
visited.add(current)
|
|
467
|
+
path = path + [current]
|
|
468
|
+
|
|
469
|
+
for relationship in self.get_outgoing(current):
|
|
470
|
+
result = self._dfs(relationship.to_id, target, visited, path, depth - 1)
|
|
471
|
+
if result:
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class MetadataIndex:
|
|
478
|
+
"""High-level metadata indexing and discovery engine."""
|
|
479
|
+
|
|
480
|
+
def __init__(self):
|
|
481
|
+
self.metadata_store = MetadataStore()
|
|
482
|
+
self.capability_discovery = CapabilityDiscovery()
|
|
483
|
+
self.schema_extractor = SchemaExtractor()
|
|
484
|
+
self.relationship_graph = RelationshipGraph()
|
|
485
|
+
|
|
486
|
+
def index_tool(
|
|
487
|
+
self,
|
|
488
|
+
name: str,
|
|
489
|
+
description: str,
|
|
490
|
+
server_id: str,
|
|
491
|
+
tool_id: str,
|
|
492
|
+
input_schema: dict,
|
|
493
|
+
output_schema: dict,
|
|
494
|
+
) -> None:
|
|
495
|
+
"""Index a tool."""
|
|
496
|
+
metadata = Metadata(
|
|
497
|
+
name=name,
|
|
498
|
+
description=description,
|
|
499
|
+
server_id=server_id,
|
|
500
|
+
tool_id=tool_id,
|
|
501
|
+
input_schema=input_schema,
|
|
502
|
+
output_schema=output_schema,
|
|
503
|
+
)
|
|
504
|
+
self.metadata_store.insert(metadata)
|
|
505
|
+
self.capability_discovery.discover_from_metadata(metadata)
|
|
506
|
+
self.schema_extractor.extract_input_output(metadata.id, input_schema, output_schema)
|
|
507
|
+
|
|
508
|
+
def search_by_name(self, query: str) -> List[Metadata]:
|
|
509
|
+
"""Search tools by name."""
|
|
510
|
+
return self.metadata_store.search_by_name(query)
|
|
511
|
+
|
|
512
|
+
def get_tools_by_server(self, server_id: str) -> List[Metadata]:
|
|
513
|
+
"""Get tools for a server."""
|
|
514
|
+
return self.metadata_store.get_by_server(server_id)
|
|
515
|
+
|
|
516
|
+
def get_capabilities_by_classification(
|
|
517
|
+
self, classification: CapabilityClassification
|
|
518
|
+
) -> List[Capability]:
|
|
519
|
+
"""Get capabilities by classification."""
|
|
520
|
+
return self.capability_discovery.get_by_classification(classification)
|
|
521
|
+
|
|
522
|
+
def get_tool_schema(self, metadata_id: str) -> List[Schema]:
|
|
523
|
+
"""Get schema for a tool."""
|
|
524
|
+
return self.schema_extractor.get_by_metadata(metadata_id)
|
|
525
|
+
|
|
526
|
+
def get_all_metadata(self) -> List[Metadata]:
|
|
527
|
+
"""Get all metadata."""
|
|
528
|
+
return self.metadata_store.all()
|
|
529
|
+
|
|
530
|
+
def stats(self) -> dict:
|
|
531
|
+
"""Get index statistics."""
|
|
532
|
+
return {
|
|
533
|
+
"total_metadata": self.metadata_store.count(),
|
|
534
|
+
"total_capabilities": self.capability_discovery.count(),
|
|
535
|
+
"total_schemas": self.schema_extractor.count(),
|
|
536
|
+
"total_relationships": self.relationship_graph.count(),
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
def bulk_index_tools(self, tools: List[dict]) -> dict:
|
|
540
|
+
"""Bulk index tools."""
|
|
541
|
+
import time
|
|
542
|
+
|
|
543
|
+
start = time.time()
|
|
544
|
+
success = 0
|
|
545
|
+
errors = 0
|
|
546
|
+
|
|
547
|
+
for tool in tools:
|
|
548
|
+
try:
|
|
549
|
+
self.index_tool(
|
|
550
|
+
name=tool["name"],
|
|
551
|
+
description=tool["description"],
|
|
552
|
+
server_id=tool["server_id"],
|
|
553
|
+
tool_id=tool["tool_id"],
|
|
554
|
+
input_schema=tool.get("input_schema", {}),
|
|
555
|
+
output_schema=tool.get("output_schema", {}),
|
|
556
|
+
)
|
|
557
|
+
success += 1
|
|
558
|
+
except Exception:
|
|
559
|
+
errors += 1
|
|
560
|
+
|
|
561
|
+
duration_ms = int((time.time() - start) * 1000)
|
|
562
|
+
return {
|
|
563
|
+
"total": success + errors,
|
|
564
|
+
"success": success,
|
|
565
|
+
"errors": errors,
|
|
566
|
+
"duration_ms": duration_ms,
|
|
567
|
+
}
|