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/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""PyStreamMCP - Intelligence Layer for AI Agents."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.3.0"
|
|
4
|
+
__author__ = "Georgi Mammen Mullassery"
|
|
5
|
+
|
|
6
|
+
from .core import (
|
|
7
|
+
MetadataIndex,
|
|
8
|
+
CapabilityDiscovery,
|
|
9
|
+
SchemaExtractor,
|
|
10
|
+
RelationshipGraph,
|
|
11
|
+
Metadata,
|
|
12
|
+
Capability,
|
|
13
|
+
Schema,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"MetadataIndex",
|
|
18
|
+
"CapabilityDiscovery",
|
|
19
|
+
"SchemaExtractor",
|
|
20
|
+
"RelationshipGraph",
|
|
21
|
+
"Metadata",
|
|
22
|
+
"Capability",
|
|
23
|
+
"Schema",
|
|
24
|
+
]
|
pystreammcp/api.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""REST API for PyStreamMCP v0.3."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from flask import Flask, jsonify, request, send_file
|
|
10
|
+
from flask_cors import CORS
|
|
11
|
+
HAS_FLASK = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
HAS_FLASK = False
|
|
14
|
+
|
|
15
|
+
from pystreammcp.core import (
|
|
16
|
+
MetadataIndex,
|
|
17
|
+
CapabilityClassification,
|
|
18
|
+
Metadata,
|
|
19
|
+
)
|
|
20
|
+
from pystreammcp.persistence import SqlitePersistence
|
|
21
|
+
from pystreammcp.routing import SemanticRouter, RoutingPlanner
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StreamMCPAPI:
|
|
25
|
+
"""REST API server for PyStreamMCP."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, db_path: str = ":memory:", debug: bool = False):
|
|
28
|
+
"""Initialize API."""
|
|
29
|
+
if not HAS_FLASK:
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"Flask is required for the API. Install with: pip install flask flask-cors"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self.db_path = db_path
|
|
35
|
+
self.app = Flask(__name__)
|
|
36
|
+
self.app.config["JSON_SORT_KEYS"] = False
|
|
37
|
+
CORS(self.app)
|
|
38
|
+
|
|
39
|
+
self.index = MetadataIndex()
|
|
40
|
+
self.persistence = SqlitePersistence(db_path)
|
|
41
|
+
self.debug = debug
|
|
42
|
+
|
|
43
|
+
self._register_routes()
|
|
44
|
+
|
|
45
|
+
def _register_routes(self):
|
|
46
|
+
"""Register API routes."""
|
|
47
|
+
|
|
48
|
+
@self.app.route("/health", methods=["GET"])
|
|
49
|
+
def health():
|
|
50
|
+
"""Health check endpoint."""
|
|
51
|
+
return jsonify(
|
|
52
|
+
{
|
|
53
|
+
"status": "ok",
|
|
54
|
+
"version": "0.3.0",
|
|
55
|
+
"db": "sqlite" if self.db_path != ":memory:" else "in-memory",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@self.app.route("/api/v1/metadata", methods=["POST"])
|
|
60
|
+
def index_tool():
|
|
61
|
+
"""Index a new tool."""
|
|
62
|
+
data = request.json
|
|
63
|
+
try:
|
|
64
|
+
name = data.get("name")
|
|
65
|
+
description = data.get("description")
|
|
66
|
+
server_id = data.get("server_id")
|
|
67
|
+
tool_id = data.get("tool_id")
|
|
68
|
+
input_schema = data.get("input_schema", {})
|
|
69
|
+
output_schema = data.get("output_schema", {})
|
|
70
|
+
|
|
71
|
+
if not all([name, server_id, tool_id]):
|
|
72
|
+
return jsonify({"error": "Missing required fields"}), 400
|
|
73
|
+
|
|
74
|
+
self.index.index_tool(
|
|
75
|
+
name=name,
|
|
76
|
+
description=description,
|
|
77
|
+
server_id=server_id,
|
|
78
|
+
tool_id=tool_id,
|
|
79
|
+
input_schema=input_schema,
|
|
80
|
+
output_schema=output_schema,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Also persist to database
|
|
84
|
+
metadata = Metadata(
|
|
85
|
+
name=name,
|
|
86
|
+
description=description,
|
|
87
|
+
server_id=server_id,
|
|
88
|
+
tool_id=tool_id,
|
|
89
|
+
input_schema=input_schema,
|
|
90
|
+
output_schema=output_schema,
|
|
91
|
+
)
|
|
92
|
+
self.persistence.insert_metadata(metadata)
|
|
93
|
+
|
|
94
|
+
return jsonify(
|
|
95
|
+
{
|
|
96
|
+
"status": "success",
|
|
97
|
+
"message": f"Tool {name} indexed",
|
|
98
|
+
"metadata_id": metadata.id,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return jsonify({"error": str(e)}), 500
|
|
103
|
+
|
|
104
|
+
@self.app.route("/api/v1/search", methods=["GET"])
|
|
105
|
+
def search():
|
|
106
|
+
"""Search for tools."""
|
|
107
|
+
query = request.args.get("q", "")
|
|
108
|
+
limit = int(request.args.get("limit", 10))
|
|
109
|
+
|
|
110
|
+
if not query:
|
|
111
|
+
return jsonify({"error": "Missing query parameter"}), 400
|
|
112
|
+
|
|
113
|
+
results = self.index.search_by_name(query)[:limit]
|
|
114
|
+
|
|
115
|
+
return jsonify(
|
|
116
|
+
{
|
|
117
|
+
"query": query,
|
|
118
|
+
"count": len(results),
|
|
119
|
+
"results": [
|
|
120
|
+
{
|
|
121
|
+
"id": m.id,
|
|
122
|
+
"name": m.name,
|
|
123
|
+
"description": m.description,
|
|
124
|
+
"server_id": m.server_id,
|
|
125
|
+
"tool_id": m.tool_id,
|
|
126
|
+
}
|
|
127
|
+
for m in results
|
|
128
|
+
],
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@self.app.route("/api/v1/route", methods=["POST"])
|
|
133
|
+
def route_query():
|
|
134
|
+
"""Route a query to optimal tools."""
|
|
135
|
+
data = request.json
|
|
136
|
+
try:
|
|
137
|
+
query = data.get("query")
|
|
138
|
+
max_tools = int(data.get("max_tools", 3))
|
|
139
|
+
|
|
140
|
+
if not query:
|
|
141
|
+
return jsonify({"error": "Missing query parameter"}), 400
|
|
142
|
+
|
|
143
|
+
all_metadata = self.index.get_all_metadata()
|
|
144
|
+
capabilities_map = {}
|
|
145
|
+
|
|
146
|
+
for metadata in all_metadata:
|
|
147
|
+
caps = self.index.capability_discovery.get_by_metadata(
|
|
148
|
+
metadata.id
|
|
149
|
+
)
|
|
150
|
+
capabilities_map[metadata.id] = caps
|
|
151
|
+
|
|
152
|
+
scores = SemanticRouter.rank_tools(query, all_metadata, capabilities_map)
|
|
153
|
+
plan = RoutingPlanner.plan(query, scores, max_tools)
|
|
154
|
+
|
|
155
|
+
return jsonify(plan.to_dict())
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return jsonify({"error": str(e)}), 500
|
|
158
|
+
|
|
159
|
+
@self.app.route("/api/v1/stats", methods=["GET"])
|
|
160
|
+
def stats():
|
|
161
|
+
"""Get index statistics."""
|
|
162
|
+
stats_data = self.index.stats()
|
|
163
|
+
return jsonify(
|
|
164
|
+
{
|
|
165
|
+
"total_metadata": stats_data["total_metadata"],
|
|
166
|
+
"total_capabilities": stats_data["total_capabilities"],
|
|
167
|
+
"total_schemas": stats_data["total_schemas"],
|
|
168
|
+
"total_relationships": stats_data["total_relationships"],
|
|
169
|
+
"db_entries": self.persistence.count_metadata(),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@self.app.route("/api/v1/metadata/<metadata_id>", methods=["GET"])
|
|
174
|
+
def get_metadata(metadata_id):
|
|
175
|
+
"""Get specific metadata."""
|
|
176
|
+
metadata = self.index.get_all_metadata()
|
|
177
|
+
for m in metadata:
|
|
178
|
+
if m.id == metadata_id or m.tool_id == metadata_id:
|
|
179
|
+
caps = self.index.capability_discovery.get_by_metadata(m.id)
|
|
180
|
+
return jsonify(
|
|
181
|
+
{
|
|
182
|
+
"id": m.id,
|
|
183
|
+
"name": m.name,
|
|
184
|
+
"description": m.description,
|
|
185
|
+
"server_id": m.server_id,
|
|
186
|
+
"tool_id": m.tool_id,
|
|
187
|
+
"capabilities": [
|
|
188
|
+
{
|
|
189
|
+
"classification": c.classification.value,
|
|
190
|
+
"inputs": c.inputs,
|
|
191
|
+
"outputs": c.outputs,
|
|
192
|
+
}
|
|
193
|
+
for c in caps
|
|
194
|
+
],
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return jsonify({"error": "Not found"}), 404
|
|
199
|
+
|
|
200
|
+
@self.app.route("/api/v1/capabilities", methods=["GET"])
|
|
201
|
+
def get_capabilities():
|
|
202
|
+
"""Get capabilities by type."""
|
|
203
|
+
classification = request.args.get("type", "search")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
cap_class = CapabilityClassification[classification.title()]
|
|
207
|
+
caps = self.index.get_capabilities_by_classification(cap_class)
|
|
208
|
+
|
|
209
|
+
return jsonify(
|
|
210
|
+
{
|
|
211
|
+
"type": classification,
|
|
212
|
+
"count": len(caps),
|
|
213
|
+
"capabilities": [
|
|
214
|
+
{
|
|
215
|
+
"id": c.id,
|
|
216
|
+
"metadata_id": c.metadata_id,
|
|
217
|
+
"inputs": c.inputs,
|
|
218
|
+
"outputs": c.outputs,
|
|
219
|
+
"cost": c.cost_estimate,
|
|
220
|
+
}
|
|
221
|
+
for c in caps
|
|
222
|
+
],
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
except KeyError:
|
|
226
|
+
return jsonify({"error": f"Unknown type: {classification}"}), 400
|
|
227
|
+
|
|
228
|
+
@self.app.route("/api/v1/servers", methods=["GET"])
|
|
229
|
+
def get_servers():
|
|
230
|
+
"""Get all unique servers."""
|
|
231
|
+
servers = {}
|
|
232
|
+
for metadata in self.index.get_all_metadata():
|
|
233
|
+
if metadata.server_id not in servers:
|
|
234
|
+
servers[metadata.server_id] = 0
|
|
235
|
+
servers[metadata.server_id] += 1
|
|
236
|
+
|
|
237
|
+
return jsonify({"servers": servers})
|
|
238
|
+
|
|
239
|
+
@self.app.errorhandler(404)
|
|
240
|
+
def not_found(error):
|
|
241
|
+
"""404 handler."""
|
|
242
|
+
return jsonify({"error": "Not found"}), 404
|
|
243
|
+
|
|
244
|
+
@self.app.errorhandler(500)
|
|
245
|
+
def server_error(error):
|
|
246
|
+
"""500 handler."""
|
|
247
|
+
return jsonify({"error": "Internal server error"}), 500
|
|
248
|
+
|
|
249
|
+
def run(self, host: str = "0.0.0.0", port: int = 8011, debug: bool = False):
|
|
250
|
+
"""Run the API server."""
|
|
251
|
+
print(f"[*] StreamMCP API v0.3 running on {host}:{port}")
|
|
252
|
+
print(f"[*] Database: {self.db_path}")
|
|
253
|
+
print(f"[*] Endpoints: /health, /api/v1/search, /api/v1/route, /api/v1/stats")
|
|
254
|
+
self.app.run(host=host, port=port, debug=debug, threaded=True)
|
|
255
|
+
|
|
256
|
+
def close(self):
|
|
257
|
+
"""Close resources."""
|
|
258
|
+
self.persistence.close()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def create_app(db_path: str = ":memory:") -> Flask:
|
|
262
|
+
"""Create Flask app."""
|
|
263
|
+
api = StreamMCPAPI(db_path)
|
|
264
|
+
return api.app
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def run_server(db_path: Optional[str] = None, port: int = 8011):
|
|
268
|
+
"""Run the REST API server."""
|
|
269
|
+
if db_path is None:
|
|
270
|
+
db_path = ":memory:"
|
|
271
|
+
|
|
272
|
+
api = StreamMCPAPI(db_path)
|
|
273
|
+
api.run(port=port, debug=False)
|
pystreammcp/cli.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""CLI for PyStreamMCP v0.3 - Metadata Indexing & Routing."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.syntax import Syntax
|
|
13
|
+
|
|
14
|
+
from pystreammcp.core import (
|
|
15
|
+
MetadataIndex,
|
|
16
|
+
CapabilityClassification,
|
|
17
|
+
RelationType,
|
|
18
|
+
Metadata,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Context:
|
|
25
|
+
"""CLI context for state management."""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.index = MetadataIndex()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
pass_context = click.make_pass_decorator(Context)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.group()
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def cli(ctx):
|
|
37
|
+
"""StreamMCP v0.2 - Intelligence Layer for MCP Systems.
|
|
38
|
+
|
|
39
|
+
Discover, index, and intelligently route queries across MCP servers.
|
|
40
|
+
"""
|
|
41
|
+
ctx.obj = Context()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cli.command()
|
|
45
|
+
@click.option("--file", "-f", type=click.Path(exists=True), help="Load tools from JSON file")
|
|
46
|
+
@click.option("--format", "-fmt", type=click.Choice(["json", "csv"]), default="json")
|
|
47
|
+
@pass_context
|
|
48
|
+
def discover(ctx: Context, file: Optional[str], format: str):
|
|
49
|
+
"""Discover MCP servers and tools.
|
|
50
|
+
|
|
51
|
+
Scans connected MCP servers and catalogs available tools.
|
|
52
|
+
"""
|
|
53
|
+
console.print("[bold cyan]🔍 MCP Server Discovery[/bold cyan]")
|
|
54
|
+
|
|
55
|
+
if file:
|
|
56
|
+
try:
|
|
57
|
+
with open(file) as f:
|
|
58
|
+
data = json.load(f)
|
|
59
|
+
tools = data.get("tools", [])
|
|
60
|
+
console.print(f"[green]✓[/green] Loaded {len(tools)} tools from {file}")
|
|
61
|
+
|
|
62
|
+
# Index tools
|
|
63
|
+
result = ctx.index.bulk_index_tools(tools)
|
|
64
|
+
console.print(f"\n[bold]Indexing Results:[/bold]")
|
|
65
|
+
console.print(f" Total: {result['total']}")
|
|
66
|
+
console.print(f" Success: [green]{result['success']}[/green]")
|
|
67
|
+
console.print(f" Errors: [red]{result['errors']}[/red]")
|
|
68
|
+
console.print(f" Duration: {result['duration_ms']}ms")
|
|
69
|
+
|
|
70
|
+
# Show stats
|
|
71
|
+
stats = ctx.index.stats()
|
|
72
|
+
console.print(f"\n[bold]Index Statistics:[/bold]")
|
|
73
|
+
console.print(f" Metadata entries: {stats['total_metadata']}")
|
|
74
|
+
console.print(f" Capabilities: {stats['total_capabilities']}")
|
|
75
|
+
console.print(f" Schemas: {stats['total_schemas']}")
|
|
76
|
+
console.print(f" Relationships: {stats['total_relationships']}")
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
console.print(f"[red]✗ File not found: {file}[/red]", file=sys.stderr)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
else:
|
|
81
|
+
console.print(
|
|
82
|
+
"[yellow]⚠ Mock discovery (provide --file for real tools)[/yellow]"
|
|
83
|
+
)
|
|
84
|
+
console.print("\n[bold]Available Servers:[/bold]")
|
|
85
|
+
console.print(" • web_server (4 tools)")
|
|
86
|
+
console.print(" • file_server (3 tools)")
|
|
87
|
+
console.print(" • database_server (5 tools)")
|
|
88
|
+
console.print(" • github_server (8 tools)")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@cli.command()
|
|
92
|
+
@click.argument("query")
|
|
93
|
+
@click.option("--classification", "-c", help="Filter by capability classification")
|
|
94
|
+
@click.option("--server", "-s", help="Filter by server")
|
|
95
|
+
@click.option("--limit", "-l", type=int, default=10, help="Max results")
|
|
96
|
+
@pass_context
|
|
97
|
+
def search(ctx: Context, query: str, classification: Optional[str], server: Optional[str], limit: int):
|
|
98
|
+
"""Search for tools by name or description.
|
|
99
|
+
|
|
100
|
+
Find relevant tools using intelligent metadata search.
|
|
101
|
+
"""
|
|
102
|
+
console.print(f"[bold cyan]🔎 Searching: '{query}'[/bold cyan]\n")
|
|
103
|
+
|
|
104
|
+
results = ctx.index.search_by_name(query)
|
|
105
|
+
|
|
106
|
+
if classification:
|
|
107
|
+
try:
|
|
108
|
+
cap_class = CapabilityClassification[classification.title()]
|
|
109
|
+
results = [
|
|
110
|
+
m
|
|
111
|
+
for m in results
|
|
112
|
+
if any(
|
|
113
|
+
c.classification == cap_class
|
|
114
|
+
for c in ctx.index.capability_discovery.get_by_metadata(m.id)
|
|
115
|
+
)
|
|
116
|
+
]
|
|
117
|
+
except KeyError:
|
|
118
|
+
console.print(f"[red]Unknown classification: {classification}[/red]", file=sys.stderr)
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
if server:
|
|
122
|
+
results = [m for m in results if m.server_id == server]
|
|
123
|
+
|
|
124
|
+
results = results[:limit]
|
|
125
|
+
|
|
126
|
+
if not results:
|
|
127
|
+
console.print("[yellow]No tools found matching your search.[/yellow]")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Show results table
|
|
131
|
+
table = Table(title=f"Search Results ({len(results)})", show_header=True)
|
|
132
|
+
table.add_column("Tool", style="cyan")
|
|
133
|
+
table.add_column("Server", style="magenta")
|
|
134
|
+
table.add_column("Description")
|
|
135
|
+
table.add_column("Type", style="yellow")
|
|
136
|
+
|
|
137
|
+
for metadata in results:
|
|
138
|
+
capabilities = ctx.index.capability_discovery.get_by_metadata(metadata.id)
|
|
139
|
+
cap_type = (
|
|
140
|
+
capabilities[0].classification.value if capabilities else "unknown"
|
|
141
|
+
)
|
|
142
|
+
table.add_row(
|
|
143
|
+
metadata.name,
|
|
144
|
+
metadata.server_id,
|
|
145
|
+
metadata.description[:50] + "..."
|
|
146
|
+
if len(metadata.description) > 50
|
|
147
|
+
else metadata.description,
|
|
148
|
+
cap_type,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
console.print(table)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@cli.command()
|
|
155
|
+
@click.argument("tool_id")
|
|
156
|
+
@pass_context
|
|
157
|
+
def inspect(ctx: Context, tool_id: str):
|
|
158
|
+
"""Inspect a specific tool in detail.
|
|
159
|
+
|
|
160
|
+
Show full metadata, schema, capabilities, and relationships.
|
|
161
|
+
"""
|
|
162
|
+
# Search for the tool
|
|
163
|
+
all_metadata = ctx.index.get_all_metadata()
|
|
164
|
+
metadata = None
|
|
165
|
+
for m in all_metadata:
|
|
166
|
+
if m.tool_id == tool_id or m.name == tool_id or m.id == tool_id:
|
|
167
|
+
metadata = m
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
if not metadata:
|
|
171
|
+
console.print(f"[red]✗ Tool not found: {tool_id}[/red]", file=sys.stderr)
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
console.print(f"\n[bold cyan]📋 Tool: {metadata.name}[/bold cyan]\n")
|
|
175
|
+
|
|
176
|
+
# Basic info
|
|
177
|
+
info = Panel(
|
|
178
|
+
f"[cyan]ID:[/cyan] {metadata.id}\n"
|
|
179
|
+
f"[cyan]Server:[/cyan] {metadata.server_id}\n"
|
|
180
|
+
f"[cyan]Tool ID:[/cyan] {metadata.tool_id}\n"
|
|
181
|
+
f"[cyan]Description:[/cyan] {metadata.description}",
|
|
182
|
+
title="Basic Info",
|
|
183
|
+
)
|
|
184
|
+
console.print(info)
|
|
185
|
+
|
|
186
|
+
# Capabilities
|
|
187
|
+
capabilities = ctx.index.capability_discovery.get_by_metadata(metadata.id)
|
|
188
|
+
if capabilities:
|
|
189
|
+
cap_text = "\n".join(
|
|
190
|
+
[
|
|
191
|
+
f"[yellow]{c.classification.value}[/yellow] "
|
|
192
|
+
f"(cost: ${c.cost_estimate:.2f}, latency: {c.latency_estimate_ms}ms)"
|
|
193
|
+
for c in capabilities
|
|
194
|
+
]
|
|
195
|
+
)
|
|
196
|
+
cap_panel = Panel(cap_text, title="Capabilities")
|
|
197
|
+
console.print(cap_panel)
|
|
198
|
+
|
|
199
|
+
# Schema
|
|
200
|
+
schemas = ctx.index.get_tool_schema(metadata.id)
|
|
201
|
+
if schemas:
|
|
202
|
+
schema_panel = Panel(
|
|
203
|
+
json.dumps([asdict(s) for s in schemas], indent=2, default=str),
|
|
204
|
+
title="Schemas",
|
|
205
|
+
)
|
|
206
|
+
console.print(schema_panel)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@cli.command()
|
|
210
|
+
@click.option("--server", "-s", help="Filter by server")
|
|
211
|
+
@pass_context
|
|
212
|
+
def stats(ctx: Context, server: Optional[str]):
|
|
213
|
+
"""Show metadata index statistics.
|
|
214
|
+
|
|
215
|
+
Display overall indexing statistics and distribution.
|
|
216
|
+
"""
|
|
217
|
+
console.print("[bold cyan]📊 Index Statistics[/bold cyan]\n")
|
|
218
|
+
|
|
219
|
+
stats_data = ctx.index.stats()
|
|
220
|
+
|
|
221
|
+
# Overall stats
|
|
222
|
+
table = Table(show_header=False)
|
|
223
|
+
table.add_row("Total Metadata", f"[cyan]{stats_data['total_metadata']}[/cyan]")
|
|
224
|
+
table.add_row("Total Capabilities", f"[green]{stats_data['total_capabilities']}[/green]")
|
|
225
|
+
table.add_row("Total Schemas", f"[yellow]{stats_data['total_schemas']}[/yellow]")
|
|
226
|
+
table.add_row(
|
|
227
|
+
"Total Relationships", f"[magenta]{stats_data['total_relationships']}[/magenta]"
|
|
228
|
+
)
|
|
229
|
+
console.print(table)
|
|
230
|
+
|
|
231
|
+
# Capability distribution
|
|
232
|
+
console.print("\n[bold]Capability Distribution:[/bold]")
|
|
233
|
+
for classification in CapabilityClassification:
|
|
234
|
+
if classification == CapabilityClassification.Unknown:
|
|
235
|
+
continue
|
|
236
|
+
count = len(ctx.index.get_capabilities_by_classification(classification))
|
|
237
|
+
bar = "█" * (count // 2)
|
|
238
|
+
console.print(f" {classification.value:12} {bar} ({count})")
|
|
239
|
+
|
|
240
|
+
# Server distribution
|
|
241
|
+
if not server:
|
|
242
|
+
console.print("\n[bold]Servers:[/bold]")
|
|
243
|
+
servers: dict = {}
|
|
244
|
+
for metadata in ctx.index.get_all_metadata():
|
|
245
|
+
servers[metadata.server_id] = servers.get(metadata.server_id, 0) + 1
|
|
246
|
+
for srv, count in sorted(servers.items()):
|
|
247
|
+
console.print(f" {srv}: {count} tools")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@cli.command()
|
|
251
|
+
@click.argument("name")
|
|
252
|
+
@click.argument("description")
|
|
253
|
+
@click.option("--server", "-s", required=True, help="Server ID")
|
|
254
|
+
@click.option("--tool-id", "-t", required=True, help="Tool ID")
|
|
255
|
+
@click.option("--input-schema", "-i", type=click.Path(exists=True), help="Input schema JSON file")
|
|
256
|
+
@click.option("--output-schema", "-o", type=click.Path(exists=True), help="Output schema JSON file")
|
|
257
|
+
@pass_context
|
|
258
|
+
def index(ctx: Context, name: str, description: str, server: str, tool_id: str, input_schema: Optional[str], output_schema: Optional[str]):
|
|
259
|
+
"""Index a new tool.
|
|
260
|
+
|
|
261
|
+
Add a tool to the metadata index.
|
|
262
|
+
"""
|
|
263
|
+
console.print(f"[bold cyan]📝 Indexing Tool: {name}[/bold cyan]\n")
|
|
264
|
+
|
|
265
|
+
# Load schemas
|
|
266
|
+
input_json = {}
|
|
267
|
+
output_json = {}
|
|
268
|
+
|
|
269
|
+
if input_schema:
|
|
270
|
+
try:
|
|
271
|
+
with open(input_schema) as f:
|
|
272
|
+
input_json = json.load(f)
|
|
273
|
+
except FileNotFoundError:
|
|
274
|
+
console.print(f"[red]Input schema file not found: {input_schema}[/red]", file=sys.stderr)
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
|
|
277
|
+
if output_schema:
|
|
278
|
+
try:
|
|
279
|
+
with open(output_schema) as f:
|
|
280
|
+
output_json = json.load(f)
|
|
281
|
+
except FileNotFoundError:
|
|
282
|
+
console.print(f"[red]Output schema file not found: {output_schema}[/red]", file=sys.stderr)
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
|
|
285
|
+
# Index
|
|
286
|
+
try:
|
|
287
|
+
ctx.index.index_tool(
|
|
288
|
+
name=name,
|
|
289
|
+
description=description,
|
|
290
|
+
server_id=server,
|
|
291
|
+
tool_id=tool_id,
|
|
292
|
+
input_schema=input_json,
|
|
293
|
+
output_schema=output_json,
|
|
294
|
+
)
|
|
295
|
+
console.print("[green]✓[/green] Tool indexed successfully")
|
|
296
|
+
stats = ctx.index.stats()
|
|
297
|
+
console.print(f"[dim]Index now contains {stats['total_metadata']} tools[/dim]")
|
|
298
|
+
except Exception as e:
|
|
299
|
+
console.print(f"[red]✗ Indexing failed: {e}[/red]", file=sys.stderr)
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@cli.command()
|
|
304
|
+
@click.option("--capabilities", "-c", is_flag=True, help="Show capability details")
|
|
305
|
+
@click.option("--schemas", "-s", is_flag=True, help="Show schema details")
|
|
306
|
+
@pass_context
|
|
307
|
+
def list_tools(ctx: Context, capabilities: bool, schemas: bool):
|
|
308
|
+
"""List all indexed tools.
|
|
309
|
+
|
|
310
|
+
Display all tools currently in the index.
|
|
311
|
+
"""
|
|
312
|
+
all_metadata = ctx.index.get_all_metadata()
|
|
313
|
+
|
|
314
|
+
if not all_metadata:
|
|
315
|
+
console.print("[yellow]No tools indexed.[/yellow]")
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
console.print(f"[bold cyan]📚 Indexed Tools ({len(all_metadata)})[/bold cyan]\n")
|
|
319
|
+
|
|
320
|
+
table = Table(show_header=True)
|
|
321
|
+
table.add_column("Name", style="cyan")
|
|
322
|
+
table.add_column("Server", style="magenta")
|
|
323
|
+
table.add_column("Type", style="yellow")
|
|
324
|
+
table.add_column("Capabilities", style="green")
|
|
325
|
+
|
|
326
|
+
for metadata in all_metadata:
|
|
327
|
+
caps = ctx.index.capability_discovery.get_by_metadata(metadata.id)
|
|
328
|
+
cap_types = ", ".join([c.classification.value for c in caps])
|
|
329
|
+
table.add_row(
|
|
330
|
+
metadata.name,
|
|
331
|
+
metadata.server_id,
|
|
332
|
+
metadata.tool_id,
|
|
333
|
+
cap_types or "unknown",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
console.print(table)
|
|
337
|
+
|
|
338
|
+
if capabilities:
|
|
339
|
+
console.print("\n[bold]Capability Details:[/bold]")
|
|
340
|
+
for metadata in all_metadata:
|
|
341
|
+
caps = ctx.index.capability_discovery.get_by_metadata(metadata.id)
|
|
342
|
+
if caps:
|
|
343
|
+
console.print(f"\n {metadata.name}:")
|
|
344
|
+
for cap in caps:
|
|
345
|
+
console.print(
|
|
346
|
+
f" • {cap.classification.value} "
|
|
347
|
+
f"(inputs: {len(cap.inputs)}, outputs: {len(cap.outputs)})"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if schemas:
|
|
351
|
+
console.print("\n[bold]Schemas:[/bold]")
|
|
352
|
+
for metadata in all_metadata:
|
|
353
|
+
schemas = ctx.index.get_tool_schema(metadata.id)
|
|
354
|
+
if schemas:
|
|
355
|
+
console.print(f"\n {metadata.name}:")
|
|
356
|
+
for schema in schemas:
|
|
357
|
+
console.print(f" • {schema.base_type.value} (props: {len(schema.properties)})")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@cli.command()
|
|
361
|
+
@pass_context
|
|
362
|
+
def version(ctx: Context):
|
|
363
|
+
"""Show version information."""
|
|
364
|
+
from streammcp import __version__
|
|
365
|
+
|
|
366
|
+
console.print(f"[bold]StreamMCP[/bold] v{__version__}")
|
|
367
|
+
console.print("[dim]Intelligence Layer for MCP Systems[/dim]")
|
|
368
|
+
console.print(f"\nPhase: v0.2 - Metadata-First Architecture")
|
|
369
|
+
console.print("Status: [yellow]In Development[/yellow]")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def main():
|
|
373
|
+
"""Entry point."""
|
|
374
|
+
try:
|
|
375
|
+
cli()
|
|
376
|
+
except KeyboardInterrupt:
|
|
377
|
+
console.print("\n[yellow]Cancelled[/yellow]")
|
|
378
|
+
sys.exit(0)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
console.print(f"[red]Error: {e}[/red]", file=sys.stderr)
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
if __name__ == "__main__":
|
|
385
|
+
main()
|