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/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
+ }