Skip to content

Commit 0468cc7

Browse files
committed
feat: add MCP server (graph, workflow, catalog, GAE tools)
- Add graph_analytics_ai/mcp/ module with FastMCP server - graph.py: get_connection_info, list_graphs, describe_graph, analyze_schema - workflow.py: start_workflow (background thread + job_id), get_workflow_status, list_workflow_jobs - catalog.py: list_epochs, get_epoch, query_executions, get_lineage, get_catalog_stats - gae.py: list_gae_engines, run_analysis, cleanup_engines (dry_run safe) - Add mcp[cli] optional extra to setup.py + gaai-mcp console script - Add mcp_config.example.json for Claude Desktop / Cursor config - Add MCP Server section to README.md - Add tests/unit/test_mcp_tools.py (10 tests, all passing)
1 parent fbfd603 commit 0468cc7

13 files changed

Lines changed: 959 additions & 0 deletions

File tree

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,76 @@ if test_connection():
648648

649649
---
650650

651+
## 🔌 MCP Server (NEW)
652+
653+
Expose the platform as an **[MCP (Model Context Protocol)](https://modelcontextprotocol.io)** server so any MCP-compatible AI host — Claude Desktop, Cursor, etc. — can call graph analytics directly as tools.
654+
655+
### Installation
656+
657+
```bash
658+
pip install ".[mcp]"
659+
```
660+
661+
### Quick Start
662+
663+
```bash
664+
# Start the MCP server
665+
gaai-mcp
666+
667+
# Or via module
668+
python -m graph_analytics_ai.mcp
669+
```
670+
671+
### Claude Desktop / Cursor Configuration
672+
673+
Copy this into your `mcp_servers.json` (see [`mcp_config.example.json`](mcp_config.example.json) for a full template):
674+
675+
```json
676+
{
677+
"graph-analytics-ai": {
678+
"command": "python",
679+
"args": ["-m", "graph_analytics_ai.mcp"],
680+
"env": {
681+
"ARANGO_ENDPOINT": "https://your-cluster:8529",
682+
"ARANGO_DATABASE": "your_db",
683+
"ARANGO_USER": "root",
684+
"ARANGO_PASSWORD": "...",
685+
"LLM_PROVIDER": "openai",
686+
"OPENAI_API_KEY": "..."
687+
}
688+
}
689+
}
690+
```
691+
692+
### Available Tools
693+
694+
| Group | Tool | Description |
695+
|-------|------|-------------|
696+
| **Graph** | `get_connection_info` | Returns ArangoDB endpoint + database (no password) |
697+
| | `list_graphs` | Lists named graphs in the configured database |
698+
| | `describe_graph` | Vertex/edge collection definitions for a graph |
699+
| | `analyze_schema` | Full schema extraction + LLM analysis |
700+
| **Workflow** | `start_workflow` | Launch an agentic workflow; returns `job_id` immediately |
701+
| | `get_workflow_status` | Poll status of a running workflow by `job_id` |
702+
| | `list_workflow_jobs` | List all jobs in the current server session |
703+
| **Catalog** | `list_epochs` | Recent analysis epochs |
704+
| | `get_epoch` | Full detail for one epoch |
705+
| | `query_executions` | Paginated execution search with filters |
706+
| | `get_lineage` | Complete lineage chain for an execution |
707+
| | `get_catalog_stats` | Summary statistics |
708+
| **GAE** | `list_gae_engines` | Active GAE engine instances |
709+
| | `run_analysis` | Run a single algorithm directly (no AI planning needed) |
710+
| | `cleanup_engines` | Remove idle/stale engines (dry-run by default) |
711+
712+
### Interactive Testing (MCP Inspector)
713+
714+
```bash
715+
# Inspector launches a browser UI at http://localhost:5173
716+
mcp dev graph_analytics_ai/mcp/server.py
717+
```
718+
719+
---
720+
651721
## Examples
652722

653723
### Example 1: Complete Workflow (Traditional)

graph_analytics_ai/mcp/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
MCP Server for Graph Analytics AI
3+
4+
Exposes the graph analytics platform as an MCP server so that any
5+
MCP-compatible AI host (Claude Desktop, Cursor, etc.) can invoke
6+
graph analytics workflows as tools.
7+
8+
Usage:
9+
python -m graph_analytics_ai.mcp
10+
11+
Or via the installed console script:
12+
gaai-mcp
13+
"""
14+
15+
from .server import mcp, create_server
16+
17+
__all__ = ["mcp", "create_server"]

graph_analytics_ai/mcp/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Allow `python -m graph_analytics_ai.mcp` to start the server."""
2+
3+
from .server import main
4+
5+
main()

graph_analytics_ai/mcp/server.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
MCP Server entry point.
3+
4+
Creates the FastMCP server instance and registers all tool groups.
5+
"""
6+
7+
from mcp.server.fastmcp import FastMCP
8+
9+
mcp = FastMCP(
10+
"graph-analytics-ai",
11+
dependencies=["python-arango", "python-dotenv", "requests"],
12+
)
13+
14+
# Register tool groups (imports trigger @mcp.tool() decorators)
15+
from .tools import graph, workflow, catalog, gae # noqa: F401, E402
16+
17+
18+
def create_server() -> FastMCP:
19+
"""Return the configured MCP server instance."""
20+
return mcp
21+
22+
23+
def main() -> None:
24+
"""Entry point for gaai-mcp console script."""
25+
mcp.run()
26+
27+
28+
if __name__ == "__main__":
29+
main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""MCP tool groups for graph-analytics-ai."""
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""
2+
Analytics Catalog tools.
3+
4+
Tools:
5+
- list_epochs – recent AnalysisEpoch records
6+
- get_epoch – full detail for one epoch
7+
- query_executions – paginated execution search with filters
8+
- get_lineage – complete lineage for an execution
9+
- get_catalog_stats – summary statistics
10+
"""
11+
12+
from typing import Optional
13+
14+
from ..server import mcp
15+
16+
17+
def _get_catalog():
18+
"""Build an AnalysisCatalog connected to the current database."""
19+
from graph_analytics_ai.db_connection import get_db_connection
20+
from graph_analytics_ai.catalog import AnalysisCatalog
21+
from graph_analytics_ai.catalog.storage import ArangoDBStorage
22+
23+
db = get_db_connection()
24+
storage = ArangoDBStorage(db)
25+
return AnalysisCatalog(storage), storage
26+
27+
28+
# ---------------------------------------------------------------------------
29+
# list_epochs
30+
# ---------------------------------------------------------------------------
31+
@mcp.tool()
32+
def list_epochs(limit: int = 20) -> list:
33+
"""List the most recent analysis epochs tracked in the catalog.
34+
35+
Args:
36+
limit: Maximum number of epochs to return (default 20).
37+
38+
Returns a list of epoch summary dicts (id, name, status, created_at).
39+
"""
40+
from graph_analytics_ai.catalog import CatalogQueries, EpochFilter
41+
42+
_, storage = _get_catalog()
43+
queries = CatalogQueries(storage)
44+
result = queries.query_with_pagination(
45+
filter=EpochFilter(),
46+
page=1,
47+
page_size=limit,
48+
)
49+
epochs = result.items if hasattr(result, "items") else result
50+
out = []
51+
for e in epochs:
52+
out.append(
53+
{
54+
"id": getattr(e, "id", None) or getattr(e, "epoch_id", None),
55+
"name": getattr(e, "name", None),
56+
"status": str(getattr(e, "status", "")),
57+
"created_at": str(getattr(e, "created_at", "")),
58+
"execution_count": getattr(e, "execution_count", None),
59+
}
60+
)
61+
return out
62+
63+
64+
# ---------------------------------------------------------------------------
65+
# get_epoch
66+
# ---------------------------------------------------------------------------
67+
@mcp.tool()
68+
def get_epoch(epoch_id: str) -> dict:
69+
"""Get full details for a single analysis epoch.
70+
71+
Args:
72+
epoch_id: The ID of the epoch to retrieve.
73+
"""
74+
catalog, _ = _get_catalog()
75+
epoch = catalog.get_epoch(epoch_id)
76+
if epoch is None:
77+
return {"error": f"Epoch {epoch_id!r} not found"}
78+
return {
79+
"id": getattr(epoch, "id", None) or getattr(epoch, "epoch_id", None),
80+
"name": getattr(epoch, "name", None),
81+
"status": str(getattr(epoch, "status", "")),
82+
"tags": getattr(epoch, "tags", []),
83+
"created_at": str(getattr(epoch, "created_at", "")),
84+
"metadata": getattr(epoch, "metadata", {}),
85+
}
86+
87+
88+
# ---------------------------------------------------------------------------
89+
# query_executions
90+
# ---------------------------------------------------------------------------
91+
@mcp.tool()
92+
def query_executions(
93+
algorithm: Optional[str] = None,
94+
epoch_id: Optional[str] = None,
95+
status: Optional[str] = None,
96+
page: int = 1,
97+
page_size: int = 20,
98+
) -> dict:
99+
"""Search execution records in the analytics catalog.
100+
101+
Args:
102+
algorithm: Filter by algorithm name (e.g. 'pagerank', 'wcc').
103+
epoch_id: Filter by epoch ID.
104+
status: Filter by execution status string (e.g. 'completed', 'failed').
105+
page: Page number (1-indexed, default 1).
106+
page_size: Results per page (default 20).
107+
108+
Returns a dict with keys: items, total, page, page_size.
109+
"""
110+
from graph_analytics_ai.catalog import CatalogQueries, ExecutionFilter
111+
112+
_, storage = _get_catalog()
113+
queries = CatalogQueries(storage)
114+
115+
f = ExecutionFilter()
116+
if algorithm:
117+
f.algorithm = algorithm
118+
if epoch_id:
119+
f.epoch_id = epoch_id
120+
if status:
121+
f.status = status
122+
123+
result = queries.query_with_pagination(filter=f, page=page, page_size=page_size)
124+
items = result.items if hasattr(result, "items") else result
125+
return {
126+
"items": [
127+
{
128+
"id": getattr(e, "id", None) or getattr(e, "execution_id", None),
129+
"algorithm": getattr(e, "algorithm", None),
130+
"status": str(getattr(e, "status", "")),
131+
"epoch_id": getattr(e, "epoch_id", None),
132+
"started_at": str(getattr(e, "started_at", "")),
133+
"duration_ms": getattr(e, "duration_ms", None),
134+
}
135+
for e in items
136+
],
137+
"total": getattr(result, "total", len(items)),
138+
"page": page,
139+
"page_size": page_size,
140+
}
141+
142+
143+
# ---------------------------------------------------------------------------
144+
# get_lineage
145+
# ---------------------------------------------------------------------------
146+
@mcp.tool()
147+
def get_lineage(execution_id: str) -> dict:
148+
"""Get the complete lineage chain for an execution.
149+
150+
Traces backwards through: Execution → Template → Use Case → Requirements.
151+
152+
Args:
153+
execution_id: The ID of the execution to trace lineage for.
154+
"""
155+
from graph_analytics_ai.catalog import LineageTracker
156+
from graph_analytics_ai.catalog.storage import ArangoDBStorage
157+
from graph_analytics_ai.db_connection import get_db_connection
158+
159+
db = get_db_connection()
160+
storage = ArangoDBStorage(db)
161+
tracker = LineageTracker(storage)
162+
lineage = tracker.get_complete_lineage(execution_id)
163+
164+
if lineage is None:
165+
return {"error": f"No lineage found for execution {execution_id!r}"}
166+
167+
return (
168+
lineage.to_dict()
169+
if hasattr(lineage, "to_dict")
170+
else {"execution_id": execution_id, "lineage": str(lineage)}
171+
)
172+
173+
174+
# ---------------------------------------------------------------------------
175+
# get_catalog_stats
176+
# ---------------------------------------------------------------------------
177+
@mcp.tool()
178+
def get_catalog_stats() -> dict:
179+
"""Return summary statistics for the analytics catalog.
180+
181+
Includes epoch count, execution count, algorithm breakdown, and more.
182+
"""
183+
from graph_analytics_ai.catalog import CatalogManager
184+
from graph_analytics_ai.catalog.storage import ArangoDBStorage
185+
from graph_analytics_ai.db_connection import get_db_connection
186+
187+
db = get_db_connection()
188+
storage = ArangoDBStorage(db)
189+
manager = CatalogManager(storage)
190+
stats = manager.get_statistics() if hasattr(manager, "get_statistics") else {}
191+
192+
return stats if isinstance(stats, dict) else (stats.to_dict() if hasattr(stats, "to_dict") else str(stats))

0 commit comments

Comments
 (0)