diff --git a/LICENSE b/LICENSE index d9f59d42..0a62d25a 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 TigerGraph Inc. + Copyright 2022-2026 TigerGraph Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index aadd9897..6cde56da 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,233 @@ # pyTigerGraph -pyTigerGraph is a Python package for connecting to TigerGraph databases. Check out the documentation [here](https://docs.tigergraph.com/pytigergraph/current/intro/). +pyTigerGraph is a Python client for [TigerGraph](https://www.tigergraph.com/) databases. It wraps the REST++ and GSQL APIs and provides both a synchronous and an asynchronous interface. -[![Downloads](https://static.pepy.tech/badge/pyTigergraph)](https://pepy.tech/project/pyTigergraph) -[![Downloads](https://static.pepy.tech/badge/pyTigergraph/month)](https://pepy.tech/project/pyTigergraph) -[![Downloads](https://static.pepy.tech/badge/pyTigergraph/week)](https://pepy.tech/project/pyTigergraph) +Full documentation: -## Quickstart +Downloads: [![Total Downloads](https://static.pepy.tech/badge/pyTigergraph)](https://pepy.tech/project/pyTigergraph) | [![Monthly Downloads](https://static.pepy.tech/badge/pyTigergraph/month)](https://pepy.tech/project/pyTigergraph) | [![Weekly Downloads](https://static.pepy.tech/badge/pyTigergraph/week)](https://pepy.tech/project/pyTigergraph) + +--- + +## Installation + +### Base package -### Installing pyTigerGraph -This section walks you through installing pyTigerGraph on your machine. +```sh +pip install pyTigerGraph +``` -#### Prerequisites -* Python 3+ -* If you wish to use the GDS functionality, install `torch` ahead of time. +### Optional extras -#### Install _pyTigerGraph_ +| Extra | What it adds | Install command | +|-------|-------------|-----------------| +| `gds` | Graph Data Science — data loaders for PyTorch Geometric, DGL, and Pandas | `pip install 'pyTigerGraph[gds]'` | +| `mcp` | Model Context Protocol server — installs [`pyTigerGraph-mcp`](https://github.com/tigergraph/tigergraph-mcp) (convenience alias) | `pip install 'pyTigerGraph[mcp]'` | +| `fast` | [orjson](https://github.com/ijl/orjson) JSON backend — 2–10× faster parsing, releases the GIL under concurrent load | `pip install 'pyTigerGraph[fast]'` | -To download _pyTigerGraph_, run the following command in the command line or use the appropriate tool of your development environment (anaconda, PyCharm, etc.).: +Extras can be combined: ```sh -pip3 install pyTigerGraph +pip install 'pyTigerGraph[fast,gds,mcp]' ``` -#### Install _pyTigerGraph[gds]_ +#### `[gds]` prerequisites -To utilize the Graph Data Science Functionality, there are a few options: -* To use the GDS functions with **PyTorch Geometric**, install `torch` and `PyTorch Geometric` according to their instructions: +Install `torch` before installing the `gds` extra: - 1) [Install Torch](https://pytorch.org/get-started/locally/) +1. [Install Torch](https://pytorch.org/get-started/locally/) +2. Optionally [Install PyTorch Geometric](https://pytorch-geometric.readthedocs.io/en/latest/notes/installation.html) or [Install DGL](https://www.dgl.ai/pages/start.html) +3. `pip install 'pyTigerGraph[gds]'` - 2) [Install PyTorch Geometric](https://pytorch-geometric.readthedocs.io/en/latest/notes/installation.html) +#### `[fast]` — orjson JSON backend - 3) Install pyTigerGraph with: - ```sh - pip3 install 'pyTigerGraph[gds]' - ``` +`orjson` is a Rust-backed JSON library that is detected and used automatically when installed. No code changes are required. It improves throughput in two ways: -* To use the GDS functions with **DGL**, install `torch` and `dgl` according to their instructions: +- **Faster parsing** — 2–10× vs stdlib `json` +- **GIL release** — threads parse responses concurrently instead of serialising on the GIL - 1) [Install Torch](https://pytorch.org/get-started/locally/) +If `orjson` is not installed the library falls back to stdlib `json` transparently. - 2) [Install DGL](https://www.dgl.ai/pages/start.html) +--- - 3) Install pyTigerGraph with: - ```sh - pip3 install 'pyTigerGraph[gds]' - ``` +## Quickstart -* To use the GDS functions without needing to produce output in the format supported by PyTorch Geometric or DGL. -This makes the data loaders output *Pandas dataframes*: -```sh -pip3 install 'pyTigerGraph[gds]' +### Synchronous connection + +```python +from pyTigerGraph import TigerGraphConnection + +conn = TigerGraphConnection( + host="http://localhost", + graphname="my_graph", + username="tigergraph", + password="tigergraph", +) + +print(conn.echo()) ``` -Once the package is installed, you can import it like any other Python package: +Use as a context manager to ensure the underlying HTTP session is closed: -```py -import pyTigerGraph as tg +```python +with TigerGraphConnection(host="http://localhost", graphname="my_graph") as conn: + result = conn.runInstalledQuery("my_query", {"param": "value"}) ``` -### Getting Started with Core Functions + +### Asynchronous connection + +`AsyncTigerGraphConnection` exposes the same API as `TigerGraphConnection` but with `async`/`await` syntax. It uses [aiohttp](https://docs.aiohttp.org/) internally and shares a single connection pool across all concurrent tasks, making it significantly more efficient than threaded sync code at high concurrency. + +```python +import asyncio +from pyTigerGraph import AsyncTigerGraphConnection + +async def main(): + async with AsyncTigerGraphConnection( + host="http://localhost", + graphname="my_graph", + username="tigergraph", + password="tigergraph", + ) as conn: + result = await conn.runInstalledQuery("my_query", {"param": "value"}) + print(result) + +asyncio.run(main()) +``` + +### Token-based authentication + +```python +conn = TigerGraphConnection( + host="http://localhost", + graphname="my_graph", + gsqlSecret="my_secret", # generates a session token automatically +) +``` + +### HTTPS / TigerGraph Cloud + +```python +conn = TigerGraphConnection( + host="https://my-instance.i.tgcloud.io", + graphname="my_graph", + username="tigergraph", + password="tigergraph", + tgCloud=True, +) +``` + +--- + +## Connection parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `host` | `str` | `"http://127.0.0.1"` | Server URL including scheme (`http://` or `https://`) | +| `graphname` | `str` | `""` | Target graph name | +| `username` | `str` | `"tigergraph"` | Database username | +| `password` | `str` | `"tigergraph"` | Database password | +| `gsqlSecret` | `str` | `""` | GSQL secret for token-based auth (preferred over username/password) | +| `apiToken` | `str` | `""` | Pre-obtained REST++ API token | +| `jwtToken` | `str` | `""` | JWT token for customer-managed authentication | +| `restppPort` | `int\|str` | `"9000"` | REST++ port (auto-fails over to `14240/restpp` for TigerGraph 4.x) | +| `gsPort` | `int\|str` | `"14240"` | GSQL server port | +| `certPath` | `str` | `None` | Path to CA certificate for HTTPS | +| `tgCloud` | `bool` | `False` | Set to `True` for TigerGraph Cloud instances | + +--- + +## Performance notes + +### Synchronous mode (`TigerGraphConnection`) + +- Each thread gets its own dedicated HTTP session and connection pool, so concurrent threads never block each other. +- Install `pyTigerGraph[fast]` to activate the `orjson` backend and reduce JSON parsing overhead under concurrent load. +- Use `ThreadPoolExecutor` to run queries in parallel: + +```python +from concurrent.futures import ThreadPoolExecutor, as_completed + +with TigerGraphConnection(...) as conn: + with ThreadPoolExecutor(max_workers=16) as executor: + futures = [executor.submit(conn.runInstalledQuery, "q", {"p": v}) for v in values] + for f in as_completed(futures): + print(f.result()) +``` + +### Asynchronous mode (`AsyncTigerGraphConnection`) + +- Uses a single `aiohttp.ClientSession` with an unbounded connection pool shared across all concurrent coroutines — no GIL, no thread-scheduling overhead. +- Typically achieves higher QPS and lower tail latency than the threaded sync mode for I/O-bound workloads. + +```python +import asyncio +from pyTigerGraph import AsyncTigerGraphConnection + +async def main(): + async with AsyncTigerGraphConnection(...) as conn: + tasks = [conn.runInstalledQuery("q", {"p": v}) for v in values] + results = await asyncio.gather(*tasks) + +asyncio.run(main()) +``` + +--- + +## Graph Data Science (GDS) + +The `gds` sub-module provides data loaders that stream vertex and edge data from TigerGraph directly into PyTorch Geometric, DGL, or Pandas DataFrames for machine learning workflows. + +Install requirements, then access via `conn.gds`: + +```python +conn = TigerGraphConnection(host="...", graphname="...") +loader = conn.gds.vertexLoader(attributes=["feat", "label"], batch_size=1024) +for batch in loader: + train(batch) +``` + +See the [GDS documentation](https://docs.tigergraph.com/pytigergraph/current/gds/) for full details. + +--- + +## MCP Server + +The TigerGraph MCP server is now a standalone package: **[pyTigerGraph-mcp](https://github.com/tigergraph/tigergraph-mcp)**. It exposes TigerGraph operations as tools for AI agents and LLM applications (Claude Desktop, Cursor, Copilot, etc.). + +```sh +# Recommended — install the standalone package directly +pip install pyTigerGraph-mcp + +# Or via the pyTigerGraph convenience alias (installs pyTigerGraph-mcp automatically) +pip install 'pyTigerGraph[mcp]' + +# Start the server (reads connection config from environment variables) +tigergraph-mcp +``` + +For full setup instructions, available tools, configuration examples, and multi-profile support, see the **[pyTigerGraph-mcp README](https://github.com/tigergraph/tigergraph-mcp#readme)**. + +> **Migrating from `pyTigerGraph.mcp`?** Update your imports: +> ```python +> # Old +> from pyTigerGraph.mcp import serve, ConnectionManager +> # New +> from tigergraph_mcp import serve, ConnectionManager +> ``` + +--- + +## Getting started video [![pyTigerGraph 101](https://img.youtube.com/vi/2BcC3C-qfX4/hqdefault.jpg)](https://www.youtube.com/watch?v=2BcC3C-qfX4) -The video above is a good starting place for learning the core functions of pyTigerGraph. [This Google Colab notebook](https://colab.research.google.com/drive/1JhYcnGVWT51KswcXZzyPzKqCoPP5htcC) is the companion notebook to the video. +Companion notebook: [Google Colab](https://colab.research.google.com/drive/1JhYcnGVWT51KswcXZzyPzKqCoPP5htcC) + +--- + +## Links + +- [Documentation](https://docs.tigergraph.com/pytigergraph/current/intro/) +- [PyPI](https://pypi.org/project/pyTigerGraph/) +- [GitHub Issues](https://github.com/tigergraph/pyTigerGraph/issues) +- [Source](https://github.com/tigergraph/pyTigerGraph) diff --git a/build.sh b/build.sh index 2c067641..5e173613 100755 --- a/build.sh +++ b/build.sh @@ -1,10 +1,57 @@ -#! /bin/bash +#!/usr/bin/env bash +set -euo pipefail -echo ---- Removing old dist ---- -rm -rf dist +usage() { + cat <&2; usage >&2; exit 1 ;; + esac + shift +done + +if $DO_BUILD; then + echo "---- Removing old dist ----" + rm -rf dist + + echo "---- Building new package ----" + python3 -m build +fi + +if $DO_UPLOAD; then + if [[ ! -d dist ]] || [[ -z "$(ls dist/)" ]]; then + echo "Error: dist/ is empty or missing. Run with --build first." >&2 + exit 1 + fi + + echo "---- Uploading to PyPI ----" + python3 -m twine upload dist/* +fi diff --git a/pyTigerGraph/__init__.py b/pyTigerGraph/__init__.py index 844db559..ca018e34 100644 --- a/pyTigerGraph/__init__.py +++ b/pyTigerGraph/__init__.py @@ -1,8 +1,13 @@ +from importlib.metadata import version as _pkg_version, PackageNotFoundError + from pyTigerGraph.pyTigerGraph import TigerGraphConnection from pyTigerGraph.pytgasync.pyTigerGraph import AsyncTigerGraphConnection from pyTigerGraph.common.exception import TigerGraphException -__version__ = "2.0.0" +try: + __version__ = _pkg_version("pyTigerGraph") +except PackageNotFoundError: + __version__ = "2.0.1" __license__ = "Apache 2" diff --git a/pyTigerGraph/common/base.py b/pyTigerGraph/common/base.py index 53e96422..ef78b5f0 100644 --- a/pyTigerGraph/common/base.py +++ b/pyTigerGraph/common/base.py @@ -15,6 +15,18 @@ from typing import Union from urllib.parse import urlparse +# orjson is an optional Rust-backed JSON library that: +# - parses/serialises 2–10× faster than stdlib json +# - releases the GIL during parsing, eliminating inter-thread contention on +# multi-threaded workloads where all threads parse responses simultaneously +# Fall back to stdlib transparently when orjson is not installed. +try: + import orjson as _orjson + _HAS_ORJSON = True +except ImportError: + _orjson = None + _HAS_ORJSON = False + from pyTigerGraph.common.exception import TigerGraphException @@ -112,6 +124,7 @@ def __init__(self, host: str = "http://127.0.0.1", graphname: str = "", # Detect auth mode automatically by checking if jwtToken or apiToken is provided self.authHeader = self._set_auth_header() + self.authMode = "token" if (self.jwtToken or self.apiToken) else "pwd" # TODO Eliminate version and use gsqlVersion only, meaning TigerGraph server version if gsqlVersion: @@ -152,6 +165,12 @@ def __init__(self, host: str = "http://127.0.0.1", graphname: str = "", self.certPath = certPath self.sslPort = str(sslPort) + # SSL verify value — depends only on useCert/certPath which are fixed after init, + # so we compute it once here rather than on every request in _prep_req. + # Note: for http, certPath="" (not None) so the condition is True → False; for + # https, useCert=True → False. The verify=True branch in _prep_req is unreachable. + self.verify = False if (self.useCert or self.certPath) else True + # TODO Remove gcp parameter if gcp: warnings.warn("The `gcp` parameter is deprecated.", @@ -212,6 +231,10 @@ def __init__(self, host: str = "http://127.0.0.1", graphname: str = "", self.asynchronous = False + # Pre-build per-authMode header dicts so _prep_req avoids repeating + # the isinstance/string-comparison chain on every request. + self._refresh_auth_headers() + logger.debug("exit: __init__") def _set_auth_header(self): @@ -223,6 +246,34 @@ def _set_auth_header(self): else: return {"Authorization": "Basic {0}".format(self.base64_credential)} + def _refresh_auth_headers(self) -> None: + """Pre-build per-authMode header dicts used by every request. + + Called once at __init__ and again after getToken() updates the + credentials. Eliminates per-request isinstance checks and string + formatting in _prep_req's hot path. + + Two dicts are kept because authMode can be either "token" or "pwd": + - "token": JWT > apiToken (tuple or str) > Basic + - "pwd": JWT > Basic + The "X-User-Agent" header is baked in so _prep_req skips that update too. + """ + # ---- token mode ---- + if isinstance(self.jwtToken, str) and self.jwtToken.strip(): + token_val = "Bearer " + self.jwtToken + elif isinstance(self.apiToken, tuple): + token_val = "Bearer " + self.apiToken[0] + elif isinstance(self.apiToken, str) and self.apiToken.strip(): + token_val = "Bearer " + self.apiToken + else: + token_val = "Basic " + self.base64_credential + + # ---- pwd mode ---- + pwd_val = ("Bearer " + self.jwtToken) if self.jwtToken else ("Basic " + self.base64_credential) + + self._cached_token_auth = {"Authorization": token_val, "X-User-Agent": "pyTigerGraph"} + self._cached_pwd_auth = {"Authorization": pwd_val, "X-User-Agent": "pyTigerGraph"} + def _verify_jwt_token_support(self): try: # Check JWT support for RestPP server @@ -281,34 +332,11 @@ def _prep_req(self, authMode, headers, url, method, data): if logger.level == logging.DEBUG: logger.debug("params: " + self._locals(locals())) - _headers = {} - - # If JWT token is provided, always use jwtToken as token - if authMode == "token": - if isinstance(self.jwtToken, str) and self.jwtToken.strip() != "": - token = self.jwtToken - elif isinstance(self.apiToken, tuple): - token = self.apiToken[0] - elif isinstance(self.apiToken, str) and self.apiToken.strip() != "": - token = self.apiToken - else: - token = None - - if token: - self.authHeader = {'Authorization': "Bearer " + token} - _headers = self.authHeader - else: - self.authHeader = { - 'Authorization': 'Basic {0}'.format(self.base64_credential)} - _headers = self.authHeader - self.authMode = "pwd" - else: - if self.jwtToken: - _headers = {'Authorization': "Bearer " + self.jwtToken} - else: - _headers = {'Authorization': 'Basic {0}'.format( - self.base64_credential)} - self.authMode = "pwd" + # Shallow-copy the pre-built header dict (auth + X-User-Agent already included). + # _refresh_auth_headers() keeps these current after every getToken() call. + _headers = dict( + self._cached_token_auth if authMode == "token" else self._cached_pwd_auth + ) if headers: _headers.update(headers) @@ -323,25 +351,28 @@ def _prep_req(self, authMode, headers, url, method, data): else: _data = None - if self.useCert is True or self.certPath is not None: - verify = False - else: - verify = True - - _headers.update({"X-User-Agent": "pyTigerGraph"}) logger.debug("exit: _prep_req") - return _headers, _data, verify + return _headers, _data, self.verify - def _parse_req(self, res, jsonResponse, strictJson, skipCheck, resKey): + def _parse_req(self, data: Union[bytes, str], jsonResponse, strictJson, skipCheck, resKey): logger.debug("entry: _parse_req") if jsonResponse: try: - res = json.loads(res.text, strict=strictJson) - except: - raise TigerGraphException("Cannot parse json: " + res.text) + if _HAS_ORJSON and strictJson: + # orjson accepts bytes directly (no decode step), parses 2–10× faster + # than stdlib, and releases the GIL — eliminating inter-thread contention + # when multiple threads parse responses simultaneously. + res = _orjson.loads(data) + else: + # strictJson=False allows control characters; orjson is always strict, + # so fall back to stdlib for that case. + res = json.loads(data, strict=strictJson) + except Exception: + text = data.decode("utf-8", errors="replace") if isinstance(data, bytes) else data + raise TigerGraphException("Cannot parse json: " + text) else: - res = res.text + res = data.decode("utf-8", errors="replace") if isinstance(data, bytes) else data if not skipCheck: self._error_check(res) diff --git a/pyTigerGraph/common/edge.py b/pyTigerGraph/common/edge.py index 71e3ec10..879763b5 100644 --- a/pyTigerGraph/common/edge.py +++ b/pyTigerGraph/common/edge.py @@ -177,30 +177,22 @@ def _dumps(data) -> str: Returns: The JSON to be sent to the endpoint. """ - ret = "" - if isinstance(data, dict): - c1 = 0 - for k1, v1 in data.items(): - if c1 > 0: - ret += "," - if k1 == ___trgvtxids: - # Dealing with the (possibly multiple instances of) edge details - # v1 should be a dict of lists - c2 = 0 - for k2, v2 in v1.items(): - if c2 > 0: - ret += "," - c3 = 0 - for v3 in v2: - if c3 > 0: - ret += "," - ret += json.dumps(k2) + ':' + json.dumps(v3) - c3 += 1 - c2 += 1 - else: - ret += json.dumps(k1) + ':' + _dumps(data[k1]) - c1 += 1 - return "{" + ret + "}" + if not isinstance(data, dict): + return json.dumps(data) + parts = [] + for k1, v1 in data.items(): + if k1 == ___trgvtxids: + # Dealing with the (possibly multiple instances of) edge details. + # v1 is a dict mapping target vertex ID -> list of attribute dicts. + # Each list entry becomes a separate JSON key:value pair (same key repeated + # for MultiEdge), so we cannot use json.dumps on v1 directly. + for k2, v2 in v1.items(): + k2_encoded = json.dumps(k2) + for v3 in v2: + parts.append(k2_encoded + ":" + json.dumps(v3)) + else: + parts.append(json.dumps(k1) + ":" + _dumps(v1)) + return "{" + ",".join(parts) + "}" def _prep_upsert_edges(sourceVertexType, edgeType, @@ -248,8 +240,8 @@ def _prep_upsert_edge_dataframe(df, from_id, to_id, attributes): for index in df.index: json_up.append(json.loads(df.loc[index].to_json())) json_up[-1] = ( - index if from_id is None else json_up[-1][from_id], - index if to_id is None else json_up[-1][to_id], + index if not from_id else json_up[-1][from_id], + index if not to_id else json_up[-1][to_id], json_up[-1] if attributes is None else {target: json_up[-1][source] for target, source in attributes.items()} ) diff --git a/pyTigerGraph/common/gsql.py b/pyTigerGraph/common/gsql.py index 2b179e2e..917a11cf 100644 --- a/pyTigerGraph/common/gsql.py +++ b/pyTigerGraph/common/gsql.py @@ -60,6 +60,69 @@ def clean_res(resp: list) -> str: return string_without_ansi +_GSQL_ERROR_PATTERNS = [ + "Encountered \"", + "SEMANTIC ERROR", + "Syntax Error", + "Failed to create", + "does not exist", + "is not a valid", + "already exists", + "Invalid syntax", +] + + +def _wrap_gsql_result(result, skipCheck: bool = False): + """Wrap a gsql() string result into a dict matching 4.x REST response format. + + Args: + result: The raw string returned by ``gsql()``. + skipCheck: If ``False`` (default), raises ``TigerGraphException`` when + an error pattern is detected — consistent with ``_error_check`` + on the 4.x REST path. If ``True``, returns the dict with + ``"error": True`` without raising. + """ + msg = str(result) if result else "" + has_error = any(p in msg for p in _GSQL_ERROR_PATTERNS) + if has_error and not skipCheck: + raise TigerGraphException(msg) + return { + "error": has_error, + "message": msg, + } + + +def _parse_graph_list(gsql_output): + """Parse ``SHOW GRAPH *`` output into a list of dicts matching 4.x REST format.""" + output = str(gsql_output) if gsql_output else "" + graphs = [] + for line in output.splitlines(): + stripped = line.strip().lstrip("- ").strip() + if not stripped.startswith("Graph "): + continue + paren_start = stripped.find("(") + name = stripped[6:paren_start].strip() if paren_start > 6 else stripped[6:].strip() + if not name or name == "*": + continue + vertices = [] + edges = [] + if paren_start != -1: + paren_end = stripped.rfind(")") + inner = stripped[paren_start + 1:paren_end] if paren_end > paren_start else "" + for token in inner.split(","): + token = token.strip() + if token.endswith(":v"): + vertices.append(token[:-2]) + elif token.endswith(":e"): + edges.append(token[:-2]) + graphs.append({ + "GraphName": name, + "VertexTypes": vertices, + "EdgeTypes": edges, + }) + return graphs + + def _prep_get_udf(ExprFunctions: bool = True, ExprUtil: bool = True): urls = {} # urls when using TG 4.x alt_urls = {} # urls when using TG 3.x diff --git a/pyTigerGraph/common/loading.py b/pyTigerGraph/common/loading.py index c12c6f56..137e70e4 100644 --- a/pyTigerGraph/common/loading.py +++ b/pyTigerGraph/common/loading.py @@ -70,9 +70,10 @@ def _prep_run_loading_job(gsUrl: str, def _prep_abort_loading_jobs(gsUrl: str, graphname: str, jobIds: list[str], pauseJob: bool): '''url builder for abortLoadingJob()''' + job_params = "&".join("jobId=" + jobId for jobId in jobIds) url = gsUrl + "/gsql/v1/loading-jobs/abort?graph=" + graphname - for jobId in jobIds: - url += "&jobId=" + jobId + if job_params: + url += "&" + job_params if pauseJob: url += "&isPause=true" return url @@ -91,16 +92,46 @@ def _prep_resume_loading_job(gsUrl: str, jobId: str): url = gsUrl + "/gsql/v1/loading-jobs/resume/" + jobId return url -def _prep_get_loading_jobs_status(gsUrl: str, jobIds: list[str]): - '''url builder for getLoadingJobStatus() - TODO: verify that this is correct - ''' - url = gsUrl + "/gsql/v1/loading-jobs/status/jobId" - for jobId in jobIds: - url += "&jobId=" + jobId +def _prep_get_loading_jobs_status(gsUrl: str, graphname: str, jobIds: list[str]): + '''url builder for getLoadingJobsStatus()''' + job_params = "&".join("jobId=" + jobId for jobId in jobIds) + url = gsUrl + "/gsql/v1/loading-jobs/status?graph=" + graphname + if job_params: + url += "&" + job_params return url -def _prep_get_loading_job_status(gsUrl: str, jobId: str): +def _prep_get_loading_job_status(gsUrl: str, graphname: str, jobId: str): '''url builder for getLoadingJobStatus()''' - url = gsUrl + "/gsql/v1/loading-jobs/status/" + jobId - return url \ No newline at end of file + url = gsUrl + "/gsql/v1/loading-jobs/status/" + jobId + "?graph=" + graphname + return url + + +# ---- Data Source helpers ---- + +def _prep_data_source_url(gsUrl: str, graphname: str = None): + '''url builder for getDataSources() and createDataSource()''' + url = gsUrl + "/gsql/v1/data-sources" + if graphname: + url += "?graph=" + graphname + return url + + +def _prep_data_source_by_name(gsUrl: str, dsName: str, graphname: str = None): + '''url builder for getDataSource(), dropDataSource(), updateDataSource()''' + url = gsUrl + "/gsql/v1/data-sources/" + dsName + if graphname: + url += "?graph=" + graphname + return url + + +def _prep_drop_all_data_sources(gsUrl: str, graphname: str = None): + '''url builder for dropAllDataSources()''' + url = gsUrl + "/gsql/v1/data-sources/dropAll" + if graphname: + url += "?graph=" + graphname + return url + + +def _prep_sample_data_url(gsUrl: str): + '''url builder for previewSampleData()''' + return gsUrl + "/gsql/v1/sample-data" \ No newline at end of file diff --git a/pyTigerGraph/common/query.py b/pyTigerGraph/common/query.py index 7b5b3655..efaed2b8 100644 --- a/pyTigerGraph/common/query.py +++ b/pyTigerGraph/common/query.py @@ -20,7 +20,11 @@ logger = logging.getLogger(__name__) # TODO getQueries() # List _all_ query names -def _parse_get_installed_queries(fmt, ret): +def _parse_get_installed_queries(fmt, ret, graphname: str = ""): + prefix = f"GET /query/{graphname}/" if graphname else "GET /query/" + if fmt == "list": + return [ep[len(prefix):] for ep in ret if ep.startswith(prefix)] + ret = {ep: v for ep, v in ret.items() if ep.startswith(prefix)} if fmt == "json": ret = json.dumps(ret) if fmt == "df": @@ -57,37 +61,33 @@ def _parse_query_parameters(params: dict) -> str: logger.debug("entry: _parseQueryParameters") logger.debug("params: " + str(params)) - ret = "" + parts = [] for k, v in params.items(): if isinstance(v, tuple): if len(v) == 2 and isinstance(v[1], str): - ret += k + "=" + str(v[0]) + "&" + k + \ - ".type=" + _safe_char(v[1]) + "&" + parts.append(k + "=" + str(v[0])) + parts.append(k + ".type=" + _safe_char(v[1])) else: raise TigerGraphException( "Invalid parameter value: (vertex_primary_id, vertex_type)" " was expected.") elif isinstance(v, list): - i = 0 - for vv in v: + for i, vv in enumerate(v): if isinstance(vv, tuple): if len(vv) == 2 and isinstance(vv[1], str): - ret += k + "[" + str(i) + "]=" + _safe_char(vv[0]) + "&" + \ - k + "[" + str(i) + "].type=" + vv[1] + "&" + parts.append(k + "[" + str(i) + "]=" + _safe_char(vv[0])) + parts.append(k + "[" + str(i) + "].type=" + vv[1]) else: raise TigerGraphException( "Invalid parameter value: (vertex_primary_id, vertex_type)" " was expected.") else: - ret += k + "=" + _safe_char(vv) + "&" - i += 1 + parts.append(k + "=" + _safe_char(vv)) elif isinstance(v, datetime): - ret += k + "=" + \ - _safe_char(v.strftime("%Y-%m-%d %H:%M:%S")) + "&" + parts.append(k + "=" + _safe_char(v.strftime("%Y-%m-%d %H:%M:%S"))) else: - ret += k + "=" + _safe_char(v) + "&" - if ret: - ret = ret[:-1] + parts.append(k + "=" + _safe_char(v)) + ret = "&".join(parts) if logger.level == logging.DEBUG: logger.debug("return: " + str(ret)) diff --git a/pyTigerGraph/mcp/MCP_README.md b/pyTigerGraph/mcp/MCP_README.md deleted file mode 100644 index a111ee58..00000000 --- a/pyTigerGraph/mcp/MCP_README.md +++ /dev/null @@ -1,393 +0,0 @@ -# pyTigerGraph MCP Support - -pyTigerGraph now includes Model Context Protocol (MCP) support, allowing AI agents to interact with TigerGraph through the MCP standard. All MCP tools use pyTigerGraph's async APIs for optimal performance. - -## Installation - -To use MCP functionality, install pyTigerGraph with the `mcp` extra: - -```bash -pip install pyTigerGraph[mcp] -``` - -This will install: -- `mcp>=1.0.0` - The MCP SDK -- `pydantic>=2.0.0` - For data validation -- `click` - For the CLI entry point -- `python-dotenv>=1.0.0` - For loading .env files - -## Usage - -### Running the MCP Server - -You can run the MCP server as a standalone process: - -```bash -tigergraph-mcp -``` - -With a custom .env file: - -```bash -tigergraph-mcp --env-file /path/to/.env -``` - -With verbose logging: - -```bash -tigergraph-mcp -v # INFO level -tigergraph-mcp -vv # DEBUG level -``` - -Or programmatically: - -```python -from pyTigerGraph.mcp import serve -import asyncio - -asyncio.run(serve()) -``` - -### Configuration - -The MCP server reads connection configuration from environment variables. You can set these either directly as environment variables or in a `.env` file. - -#### Using a .env File (Recommended) - -Create a `.env` file in your project directory: - -```bash -# .env -TG_HOST=http://localhost -TG_GRAPHNAME=MyGraph # Optional - can be omitted if database has multiple graphs -TG_USERNAME=tigergraph -TG_PASSWORD=tigergraph -TG_RESTPP_PORT=9000 -TG_GS_PORT=14240 -``` - -The server will automatically load the `.env` file if it exists. Environment variables take precedence over `.env` file values. - -You can also specify a custom path to the `.env` file: - -```bash -tigergraph-mcp --env-file /path/to/custom/.env -``` - -#### Environment Variables - -The following environment variables are supported: - -- `TG_HOST` - TigerGraph host (default: http://127.0.0.1) -- `TG_GRAPHNAME` - Graph name (optional - can be omitted if database has multiple graphs. Use `tigergraph__list_graphs` tool to see available graphs) -- `TG_USERNAME` - Username (default: tigergraph) -- `TG_PASSWORD` - Password (default: tigergraph) -- `TG_SECRET` - GSQL secret (optional) -- `TG_API_TOKEN` - API token (optional) -- `TG_JWT_TOKEN` - JWT token (optional) -- `TG_RESTPP_PORT` - REST++ port (default: 9000) -- `TG_GS_PORT` - GSQL port (default: 14240) -- `TG_SSL_PORT` - SSL port (default: 443) -- `TG_TGCLOUD` - Whether using TigerGraph Cloud (default: False) -- `TG_CERT_PATH` - Path to certificate (optional) - -### Using with Existing Connection - -You can also use MCP with an existing `TigerGraphConnection` (sync) or `AsyncTigerGraphConnection`: - -**With Sync Connection:** -```python -from pyTigerGraph import TigerGraphConnection - -conn = TigerGraphConnection( - host="http://localhost", - graphname="MyGraph", - username="tigergraph", - password="tigergraph" -) - -# Enable MCP support for this connection -# This creates an async connection internally for MCP tools -conn.start_mcp_server() -``` - -**With Async Connection (Recommended):** -```python -from pyTigerGraph import AsyncTigerGraphConnection -from pyTigerGraph.mcp import ConnectionManager - -conn = AsyncTigerGraphConnection( - host="http://localhost", - graphname="MyGraph", - username="tigergraph", - password="tigergraph" -) - -# Set as default for MCP tools -ConnectionManager.set_default_connection(conn) -``` - -This sets the connection as the default for MCP tools. Note that MCP tools use async APIs internally, so using `AsyncTigerGraphConnection` directly is more efficient. - -## Available Tools - -The MCP server provides the following tools: - -### Global Schema Operations (Database Level) -These operations work with the global schema that spans across the entire TigerGraph database. - -- `tigergraph__get_global_schema` - Get the complete global schema (all global vertex/edge types, graphs, and members) via GSQL 'LS' command - -### Graph Operations (Database Level) -These operations manage individual graphs within the TigerGraph database. A database can contain multiple graphs. - -- `tigergraph__list_graphs` - List all graph names in the database (names only, no details) -- `tigergraph__create_graph` - Create a new graph with its schema (vertex types, edge types) -- `tigergraph__drop_graph` - Drop (delete) a graph and its schema -- `tigergraph__clear_graph_data` - Clear all data from a graph (keeps schema structure) - -### Schema Operations (Graph Level) -These operations work with the schema and objects of a specific graph. - -- `tigergraph__get_graph_schema` - Get the schema of a specific graph as structured JSON (vertex/edge types and attributes only) -- `tigergraph__show_graph_details` - Show details of a graph: schema, queries, loading jobs, data sources. Use `detail_type` to filter (`schema`, `query`, `loading_job`, `data_source`) or omit for all - -### Node Operations -- `tigergraph__add_node` - Add a single node -- `tigergraph__add_nodes` - Add multiple nodes -- `tigergraph__get_node` - Get a single node -- `tigergraph__get_nodes` - Get multiple nodes -- `tigergraph__delete_node` - Delete a single node -- `tigergraph__delete_nodes` - Delete multiple nodes -- `tigergraph__has_node` - Check if a node exists -- `tigergraph__get_node_edges` - Get all edges connected to a node - -### Edge Operations -- `tigergraph__add_edge` - Add a single edge -- `tigergraph__add_edges` - Add multiple edges -- `tigergraph__get_edge` - Get a single edge -- `tigergraph__get_edges` - Get multiple edges -- `tigergraph__delete_edge` - Delete a single edge -- `tigergraph__delete_edges` - Delete multiple edges -- `tigergraph__has_edge` - Check if an edge exists - -### Query Operations -- `tigergraph__run_query` - Run an interpreted query -- `tigergraph__run_installed_query` - Run an installed query -- `tigergraph__install_query` - Install a query -- `tigergraph__drop_query` - Drop (delete) an installed query -- `tigergraph__show_query` - Show query text -- `tigergraph__get_query_metadata` - Get query metadata -- `tigergraph__is_query_installed` - Check if a query is installed -- `tigergraph__get_neighbors` - Get neighbor vertices of a node - -### Loading Job Operations -- `tigergraph__create_loading_job` - Create a loading job from structured config (file mappings, node/edge mappings) -- `tigergraph__run_loading_job_with_file` - Execute a loading job with a data file -- `tigergraph__run_loading_job_with_data` - Execute a loading job with inline data string -- `tigergraph__get_loading_jobs` - Get all loading jobs for the graph -- `tigergraph__get_loading_job_status` - Get status of a specific loading job -- `tigergraph__drop_loading_job` - Drop a loading job - -### Statistics Operations -- `tigergraph__get_vertex_count` - Get vertex count -- `tigergraph__get_edge_count` - Get edge count -- `tigergraph__get_node_degree` - Get the degree (number of edges) of a node - -### GSQL Operations -- `tigergraph__gsql` - Execute raw GSQL command -- `tigergraph__generate_gsql` - Generate a GSQL query from a natural language description (requires LLM configuration) -- `tigergraph__generate_cypher` - Generate an openCypher query from a natural language description (requires LLM configuration) - -### Vector Schema Operations -- `tigergraph__add_vector_attribute` - Add a vector attribute to a vertex type (DIMENSION, METRIC: COSINE/L2/IP) -- `tigergraph__drop_vector_attribute` - Drop a vector attribute from a vertex type -- `tigergraph__list_vector_attributes` - List vector attributes (name, dimension, index type, data type, metric) by parsing `LS` output; optionally filter by vertex type -- `tigergraph__get_vector_index_status` - Check vector index rebuild status (Ready_for_query/Rebuild_processing) - -### Vector Data Operations -- `tigergraph__upsert_vectors` - Upsert multiple vertices with vector data using REST API (batch support) -- `tigergraph__load_vectors_from_csv` - Bulk-load vectors from a CSV/delimited file via a GSQL loading job (creates job, runs with file, drops job) -- `tigergraph__load_vectors_from_json` - Bulk-load vectors from a JSON Lines (.jsonl) file via a GSQL loading job with `JSON_FILE="true"` (creates job, runs with file, drops job) -- `tigergraph__search_top_k_similarity` - Perform vector similarity search using `vectorSearch()` function -- `tigergraph__fetch_vector` - Fetch vertices with vector data using GSQL `PRINT WITH VECTOR` - -**Note:** Vector attributes can ONLY be fetched via GSQL queries with `PRINT v WITH VECTOR;` - they cannot be retrieved via REST API. - -### Data Source Operations -- `tigergraph__create_data_source` - Create a new data source (S3, GCS, Azure Blob, local) -- `tigergraph__update_data_source` - Update an existing data source -- `tigergraph__get_data_source` - Get information about a data source -- `tigergraph__drop_data_source` - Drop a data source -- `tigergraph__get_all_data_sources` - Get all data sources -- `tigergraph__drop_all_data_sources` - Drop all data sources -- `tigergraph__preview_sample_data` - Preview sample data from a file - -### Discovery & Navigation -- `tigergraph__discover_tools` - Search for tools by description, use case, or keywords -- `tigergraph__get_workflow` - Get step-by-step workflow templates for common tasks (e.g., `data_loading`, `schema_creation`, `graph_exploration`) -- `tigergraph__get_tool_info` - Get detailed information about a specific tool (parameters, examples, related tools) - -## Backward Compatibility - -All existing pyTigerGraph APIs continue to work as before. MCP support is completely optional and does not affect existing code. The MCP functionality is only available when: - -1. The `mcp` extra is installed -2. You explicitly use MCP-related imports or methods - -## Example: Using with MCP Clients - -### Using MultiServerMCPClient - -```python -from langchain_mcp_adapters import MultiServerMCPClient -from pathlib import Path -from dotenv import dotenv_values -import asyncio - -# Load environment variables -env_dict = dotenv_values(dotenv_path=Path(".env").expanduser().resolve()) - -# Configure the client -client = MultiServerMCPClient( - { - "tigergraph-mcp": { - "transport": "stdio", - "command": "tigergraph-mcp", - "args": ["-vv"], # Enable debug logging - "env": env_dict, - }, - } -) - -# Get tools and use them -tools = asyncio.run(client.get_tools()) -# Tools are now available for use -``` - -### Using MCP Client SDK Directly - -```python -import asyncio -from mcp import ClientSession, StdioServerParameters -from mcp.client.stdio import stdio_client - -async def call_tool(): - # Configure server parameters - server_params = StdioServerParameters( - command="tigergraph-mcp", - args=["-vv"], # Enable debug logging - env=None, # Uses .env file or environment variables - ) - - async with stdio_client(server_params) as (read, write): - async with ClientSession(read, write) as session: - await session.initialize() - - # List available tools - tools = await session.list_tools() - print(f"Available tools: {[t.name for t in tools.tools]}") - - # Call a tool - result = await session.call_tool( - "tigergraph__list_graphs", - arguments={} - ) - - # Print result - for content in result.content: - print(content.text) - -asyncio.run(call_tool()) -``` - -**Note:** When using `MultiServerMCPClient` or similar MCP clients with stdio transport, the `args` parameter is required. For the `tigergraph-mcp` command (which is a standalone entry point), set `args` to an empty list `[]`. If you need to pass arguments to the command, include them in the list (e.g., `["-v"]` for verbose mode, `["-vv"]` for debug mode). - -## LLM-Friendly Features - -The MCP server is designed to help AI agents work effectively with TigerGraph. - -### Structured Responses - -Every tool response follows a consistent JSON structure: - -```json -{ - "success": true, - "operation": "get_node", - "summary": "Found vertex 'p123' of type 'Person'", - "data": { ... }, - "suggestions": [ - "View connected edges: get_node_edges(...)", - "Find neighbors: get_neighbors(...)" - ], - "metadata": { "graph_name": "MyGraph" } -} -``` - -Error responses include actionable recovery hints: - -```json -{ - "success": false, - "operation": "get_node", - "error": "Vertex not found", - "suggestions": [ - "Verify the vertex_id is correct", - "Check vertex type with show_graph_details()" - ] -} -``` - -### Rich Tool Descriptions - -Each tool includes detailed descriptions with: -- **Use cases** — when to call this tool -- **Common workflows** — step-by-step patterns -- **Tips** — best practices and gotchas -- **Warnings** — safety notes for destructive operations -- **Related tools** — what to call next - -### Token Optimization - -Responses are designed for efficient LLM token usage: -- No echoing of input parameters (the LLM already knows what it sent) -- Only returns new information (results, counts, boolean answers) -- Clean text output with no decorative formatting - -## Tool Discovery Workflow - -The MCP server includes discovery tools to help AI agents find the right tool for a task: - -```python -# 1. Discover tools for a task -result = await session.call_tool( - "tigergraph__discover_tools", - arguments={"query": "how to add data to the graph"} -) -# Returns: ranked list of relevant tools with use cases - -# 2. Get a workflow template -result = await session.call_tool( - "tigergraph__get_workflow", - arguments={"workflow_type": "data_loading"} -) -# Returns: step-by-step guide with tool calls - -# 3. Get detailed tool info -result = await session.call_tool( - "tigergraph__get_tool_info", - arguments={"tool_name": "tigergraph__add_node"} -) -# Returns: full documentation, examples, related tools -``` - -## Notes - -- **Async APIs**: All MCP tools use pyTigerGraph's async APIs (`AsyncTigerGraphConnection`) for optimal performance -- **Transport**: The MCP server uses stdio transport by default -- **Structured Responses**: All tools return structured JSON responses with `success`, `operation`, `summary`, `data`, `suggestions`, and `metadata` fields. Error responses include recovery hints and contextual suggestions -- **Error Detection**: GSQL operations include error detection for syntax and semantic errors (since `conn.gsql()` does not raise Python exceptions for GSQL failures) -- **Connection Management**: The connection manager automatically creates async connections from environment variables -- **Performance**: Async APIs for non-blocking I/O; `v.outdegree()` for O(1) degree counting; batch operations for multiple vertices/edges - diff --git a/pyTigerGraph/mcp/__init__.py b/pyTigerGraph/mcp/__init__.py index b5b4dbd8..853bac00 100644 --- a/pyTigerGraph/mcp/__init__.py +++ b/pyTigerGraph/mcp/__init__.py @@ -1,18 +1,44 @@ # Copyright 2025 TigerGraph Inc. # Licensed under the Apache License, Version 2.0. # See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. -"""Model Context Protocol (MCP) support for TigerGraph. +"""Deprecated MCP shim — the MCP server has moved to the `pyTigerGraph-mcp` package. -This module provides MCP server capabilities for TigerGraph, allowing -AI agents to interact with TigerGraph through the Model Context Protocol. +Install the standalone package:: + + pip install pyTigerGraph-mcp + +Or continue using the convenience alias (which installs `pyTigerGraph-mcp` automatically):: + + pip install pyTigerGraph[mcp] + +Update your imports:: + + # Old + from pyTigerGraph.mcp import serve, MCPServer, ConnectionManager + + # New + from tigergraph_mcp import serve, MCPServer, ConnectionManager """ -from .server import serve, MCPServer -from .connection_manager import get_connection, ConnectionManager +import warnings + +warnings.warn( + "pyTigerGraph.mcp is deprecated and will be removed in a future release. " + "The MCP server now lives in the 'pyTigerGraph-mcp' package. " + "Install it with: pip install pyTigerGraph-mcp " + "Update imports from 'pyTigerGraph.mcp' to 'tigergraph_mcp'.", + DeprecationWarning, + stacklevel=2, +) + +try: + from tigergraph_mcp import serve, MCPServer, get_connection, ConnectionManager # noqa: F401 +except ImportError as e: + raise ImportError( + "Could not import 'tigergraph_mcp'. " + "Install it with: pip install pyTigerGraph-mcp" + ) from e __all__ = [ "serve", @@ -20,4 +46,3 @@ "get_connection", "ConnectionManager", ] - diff --git a/pyTigerGraph/mcp/connection_manager.py b/pyTigerGraph/mcp/connection_manager.py deleted file mode 100644 index f87c02dd..00000000 --- a/pyTigerGraph/mcp/connection_manager.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""Connection manager for MCP server. - -Manages AsyncTigerGraphConnection instances for MCP tools. -""" - -import os -import logging -from pathlib import Path -from typing import Optional, Dict, Any -from pyTigerGraph import AsyncTigerGraphConnection -from pyTigerGraph.common.exception import TigerGraphException - -logger = logging.getLogger(__name__) - -# Try to load dotenv if available -try: - from dotenv import load_dotenv - _dotenv_available = True -except ImportError: - _dotenv_available = False - - -def _load_env_file(env_path: Optional[str] = None) -> None: - """Load environment variables from .env file if available. - - Args: - env_path: Optional path to .env file. If not provided, looks for .env in current directory. - """ - if not _dotenv_available: - return - - if env_path: - env_file = Path(env_path).expanduser().resolve() - else: - # Look for .env in current directory and parent directories - current_dir = Path.cwd() - env_file = None - for directory in [current_dir] + list(current_dir.parents): - potential_env = directory / ".env" - if potential_env.exists(): - env_file = potential_env - break - - if env_file is None: - # Also check in the directory where the script is running - env_file = Path(".env") - - if env_file and env_file.exists(): - load_dotenv(env_file, override=False) # Don't override existing env vars - logger.debug(f"Loaded environment variables from {env_file}") - elif env_path: - logger.warning(f"Specified .env file not found: {env_path}") - - -class ConnectionManager: - """Manages TigerGraph connections for MCP tools.""" - - _default_connection: Optional[AsyncTigerGraphConnection] = None - - @classmethod - def get_default_connection(cls) -> Optional[AsyncTigerGraphConnection]: - """Get the default connection instance.""" - return cls._default_connection - - @classmethod - def set_default_connection(cls, conn: AsyncTigerGraphConnection) -> None: - """Set the default connection instance.""" - cls._default_connection = conn - - @classmethod - def create_connection_from_env(cls, env_path: Optional[str] = None) -> AsyncTigerGraphConnection: - """Create a connection from environment variables. - - Automatically loads variables from a .env file if it exists (requires python-dotenv). - Environment variables take precedence over .env file values. - - Reads the following environment variables: - - TG_HOST: TigerGraph host (default: http://127.0.0.1) - - TG_GRAPHNAME: Graph name (optional - can be set later or use list_graphs tool) - - TG_USERNAME: Username (default: tigergraph) - - TG_PASSWORD: Password (default: tigergraph) - - TG_SECRET: GSQL secret (optional) - - TG_API_TOKEN: API token (optional) - - TG_JWT_TOKEN: JWT token (optional) - - TG_RESTPP_PORT: REST++ port (default: 9000) - - TG_GS_PORT: GSQL port (default: 14240) - - TG_SSL_PORT: SSL port (default: 443) - - TG_TGCLOUD: Whether using TigerGraph Cloud (default: False) - - TG_CERT_PATH: Path to certificate (optional) - - Args: - env_path: Optional path to .env file. If not provided, searches for .env in current and parent directories. - """ - # Load .env file if available - _load_env_file(env_path) - - host = os.getenv("TG_HOST", "http://127.0.0.1") - graphname = os.getenv("TG_GRAPHNAME", "") # Optional - can be empty - username = os.getenv("TG_USERNAME", "tigergraph") - password = os.getenv("TG_PASSWORD", "tigergraph") - gsql_secret = os.getenv("TG_SECRET", "") - api_token = os.getenv("TG_API_TOKEN", "") - jwt_token = os.getenv("TG_JWT_TOKEN", "") - restpp_port = os.getenv("TG_RESTPP_PORT", "9000") - gs_port = os.getenv("TG_GS_PORT", "14240") - ssl_port = os.getenv("TG_SSL_PORT", "443") - tg_cloud = os.getenv("TG_TGCLOUD", "false").lower() == "true" - cert_path = os.getenv("TG_CERT_PATH", None) - - # TG_GRAPHNAME is now optional - can be set later or use list_graphs tool - - conn = AsyncTigerGraphConnection( - host=host, - graphname=graphname, - username=username, - password=password, - gsqlSecret=gsql_secret if gsql_secret else "", - apiToken=api_token if api_token else "", - jwtToken=jwt_token if jwt_token else "", - restppPort=restpp_port, - gsPort=gs_port, - sslPort=ssl_port, - tgCloud=tg_cloud, - certPath=cert_path, - ) - - cls._default_connection = conn - return conn - - -def get_connection( - graph_name: Optional[str] = None, - connection_config: Optional[Dict[str, Any]] = None, -) -> AsyncTigerGraphConnection: - """Get or create an async TigerGraph connection. - - Args: - graph_name: Name of the graph. If provided, will create a new connection. - connection_config: Connection configuration dict. If provided, will create a new connection. - - Returns: - AsyncTigerGraphConnection instance. - """ - # If connection config is provided, create a new connection - if connection_config: - return AsyncTigerGraphConnection( - host=connection_config.get("host", "http://127.0.0.1"), - graphname=connection_config.get("graphname", graph_name or ""), - username=connection_config.get("username", "tigergraph"), - password=connection_config.get("password", "tigergraph"), - gsqlSecret=connection_config.get("gsqlSecret", ""), - apiToken=connection_config.get("apiToken", ""), - jwtToken=connection_config.get("jwtToken", ""), - restppPort=connection_config.get("restppPort", "9000"), - gsPort=connection_config.get("gsPort", "14240"), - sslPort=connection_config.get("sslPort", "443"), - tgCloud=connection_config.get("tgCloud", False), - certPath=connection_config.get("certPath", None), - ) - - # If graph_name is provided, try to get/create connection for that graph - if graph_name: - # For now, use default connection but set graphname - conn = ConnectionManager.get_default_connection() - if conn is None: - conn = ConnectionManager.create_connection_from_env() - # Update graphname if different - if conn.graphname != graph_name: - conn.graphname = graph_name - return conn - - # Return default connection or create from env - conn = ConnectionManager.get_default_connection() - if conn is None: - conn = ConnectionManager.create_connection_from_env() - return conn - diff --git a/pyTigerGraph/mcp/main.py b/pyTigerGraph/mcp/main.py deleted file mode 100644 index 75056af4..00000000 --- a/pyTigerGraph/mcp/main.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""Main entry point for TigerGraph MCP server.""" - -import logging -import sys -import click -import asyncio -from pathlib import Path - -from .server import serve - - -@click.command() -@click.option("-v", "--verbose", count=True) -@click.option("--env-file", type=click.Path(exists=True, path_type=Path), default=None, - help="Path to .env file (default: searches for .env in current and parent directories)") -def main(verbose: bool, env_file: Path = None) -> None: - """TigerGraph MCP Server - TigerGraph functionality for MCP - - The server will automatically load environment variables from a .env file - if python-dotenv is installed and a .env file is found. - """ - - logging_level = logging.WARN - if verbose == 1: - logging_level = logging.INFO - elif verbose >= 2: - logging_level = logging.DEBUG - - logging.basicConfig(level=logging_level, stream=sys.stderr) - - # Ensure mcp.server.lowlevel.server respects the WARNING level - logging.getLogger('mcp.server.lowlevel.server').setLevel(logging.WARNING) - - # Load .env file (automatically searches if not specified) - from .connection_manager import _load_env_file - if env_file: - _load_env_file(str(env_file)) - else: - # Automatically search for .env file - _load_env_file() - - asyncio.run(serve()) - - -if __name__ == "__main__": - main() - diff --git a/pyTigerGraph/mcp/response_formatter.py b/pyTigerGraph/mcp/response_formatter.py deleted file mode 100644 index 00b99f5e..00000000 --- a/pyTigerGraph/mcp/response_formatter.py +++ /dev/null @@ -1,315 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""Structured response formatting for MCP tools. - -This module provides utilities for creating consistent, LLM-friendly responses -from MCP tools. It ensures responses are both machine-readable and human-friendly. -""" - -import json -from typing import Any, Dict, List, Optional -from datetime import datetime -from pydantic import BaseModel -from mcp.types import TextContent - - -class ToolResponse(BaseModel): - """Structured response format for all MCP tools. - - This format provides: - - Clear success/failure indication - - Structured data for parsing - - Human-readable summary - - Contextual suggestions for next steps - - Rich metadata - """ - success: bool - operation: str - data: Optional[Dict[str, Any]] = None - summary: str - metadata: Optional[Dict[str, Any]] = None - suggestions: Optional[List[str]] = None - error: Optional[str] = None - error_code: Optional[str] = None - timestamp: str = None - - def __init__(self, **data): - if 'timestamp' not in data: - data['timestamp'] = datetime.utcnow().isoformat() + 'Z' - super().__init__(**data) - - -def format_response( - success: bool, - operation: str, - summary: str, - data: Optional[Dict[str, Any]] = None, - suggestions: Optional[List[str]] = None, - error: Optional[str] = None, - error_code: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, -) -> List[TextContent]: - """Create a structured response for MCP tools. - - Args: - success: Whether the operation succeeded - operation: Name of the operation (tool name without prefix) - summary: Human-readable summary message - data: Structured result data - suggestions: List of suggested next steps or actions - error: Error message if success=False - error_code: Optional error code for categorization - metadata: Additional context (graph_name, timing, etc.) - - Returns: - List of TextContent with both JSON and formatted text - - Example: - >>> format_response( - ... success=True, - ... operation="add_node", - ... summary="Node added successfully", - ... data={"vertex_id": "user1", "vertex_type": "Person"}, - ... suggestions=["Use 'get_node' to verify", "Use 'add_edge' to connect"] - ... ) - """ - - response = ToolResponse( - success=success, - operation=operation, - summary=summary, - data=data, - suggestions=suggestions, - error=error, - error_code=error_code, - metadata=metadata - ) - - # Create structured JSON output - json_output = response.model_dump_json(indent=2, exclude_none=True) - - # Create human-readable format - text_parts = [f"**{summary}**"] - - # Add data section - if data: - text_parts.append(f"\n**Data:**\n```json\n{json.dumps(data, indent=2, default=str)}\n```") - - # Add suggestions - if suggestions and len(suggestions) > 0: - text_parts.append("\n**💡 Suggestions:**") - for i, suggestion in enumerate(suggestions, 1): - text_parts.append(f"{i}. {suggestion}") - - # Add error details - if error: - text_parts.append(f"\n**❌ Error Details:**\n{error}") - if error_code: - text_parts.append(f"\n**Error Code:** {error_code}") - - # Add metadata footer - if metadata: - text_parts.append(f"\n**Metadata:** {json.dumps(metadata, default=str)}") - - text_output = "\n".join(text_parts) - - # Combine both formats - full_output = f"```json\n{json_output}\n```\n\n{text_output}" - - return [TextContent(type="text", text=full_output)] - - -def format_success( - operation: str, - summary: str, - data: Optional[Dict[str, Any]] = None, - suggestions: Optional[List[str]] = None, - metadata: Optional[Dict[str, Any]] = None, -) -> List[TextContent]: - """Convenience method for successful operations.""" - return format_response( - success=True, - operation=operation, - summary=summary, - data=data, - suggestions=suggestions, - metadata=metadata - ) - - -def format_error( - operation: str, - error: Exception, - context: Optional[Dict[str, Any]] = None, - suggestions: Optional[List[str]] = None, -) -> List[TextContent]: - """Format an error response with contextual recovery hints. - - Args: - operation: Name of the failed operation - error: The exception that occurred - context: Context information (parameters, state, etc.) - suggestions: Optional manual suggestions (auto-generated if not provided) - - Returns: - Formatted error response with recovery hints - """ - - error_str = str(error) - error_lower = error_str.lower() - - # Auto-generate suggestions based on error type if not provided - if suggestions is None: - suggestions = [] - - # Schema/type errors - if any(term in error_lower for term in ["vertex type", "edge type", "type not found"]): - suggestions.extend([ - "The specified type may not exist in the schema", - "Call 'show_graph_details' to see available vertex and edge types", - "Call 'list_graphs' to ensure you're using the correct graph" - ]) - - # Attribute errors - elif any(term in error_lower for term in ["attribute", "column", "field"]): - suggestions.extend([ - "One or more attributes may not match the schema definition", - "Call 'show_graph_details' to see required attributes and their types", - "Check that attribute names are spelled correctly" - ]) - - # Connection errors - elif any(term in error_lower for term in ["connection", "timeout", "unreachable"]): - suggestions.extend([ - "Unable to connect to TigerGraph server", - "Verify TG_HOST environment variable is correct", - "Check network connectivity and firewall settings", - "Ensure TigerGraph server is running" - ]) - - # Authentication errors - elif any(term in error_lower for term in ["auth", "token", "permission", "forbidden"]): - suggestions.extend([ - "Authentication failed - check credentials", - "Verify TG_USERNAME and TG_PASSWORD environment variables", - "For TigerGraph Cloud, ensure TG_API_TOKEN is set", - "Check if user has required permissions for this operation" - ]) - - # Query errors - elif any(term in error_lower for term in ["syntax", "parse", "query"]): - suggestions.extend([ - "Query syntax error detected", - "For GSQL: Use 'INTERPRET QUERY () FOR GRAPH { ... }'", - "For Cypher: Use 'INTERPRET OPENCYPHER QUERY () FOR GRAPH { ... }'", - "Call 'show_graph_details' to understand the schema before writing queries" - ]) - - # Vector errors - elif any(term in error_lower for term in ["vector", "dimension", "embedding"]): - suggestions.extend([ - "Vector operation error", - "Ensure vector dimensions match the attribute definition", - "Call 'get_vector_index_status' to check if index is ready", - "Verify vector attribute exists with 'show_graph_details'" - ]) - - # Generic suggestions - if len(suggestions) == 0: - suggestions.extend([ - "Check the error message for specific details", - "Call 'show_graph_details' to understand the current graph structure", - "Verify all required parameters are provided correctly" - ]) - - # Determine error code - error_code = None - if "connection" in error_lower or "timeout" in error_lower: - error_code = "CONNECTION_ERROR" - elif "auth" in error_lower or "permission" in error_lower: - error_code = "AUTHENTICATION_ERROR" - elif "type" in error_lower: - error_code = "SCHEMA_ERROR" - elif "attribute" in error_lower: - error_code = "ATTRIBUTE_ERROR" - elif "syntax" in error_lower or "parse" in error_lower: - error_code = "SYNTAX_ERROR" - else: - error_code = "OPERATION_ERROR" - - return format_response( - success=False, - operation=operation, - summary=f"❌ Failed to {operation.replace('_', ' ')}", - error=error_str, - error_code=error_code, - metadata=context, - suggestions=suggestions - ) - - -def gsql_has_error(result_str: str) -> bool: - """Check whether a GSQL result string indicates a failure. - - ``conn.gsql()`` does **not** raise an exception when a GSQL command fails; - instead, the error message is returned as a plain string. This helper - inspects the result for well-known error patterns so callers can - distinguish success from failure. - """ - error_patterns = [ - "Encountered \"", - "SEMANTIC ERROR", - "Syntax Error", - "Failed to create", - "does not exist", - "is not a valid", - "already exists", - "Invalid syntax", - ] - return any(p in result_str for p in error_patterns) - - -def format_list_response( - operation: str, - items: List[Any], - item_type: str = "items", - summary_template: Optional[str] = None, - suggestions: Optional[List[str]] = None, - metadata: Optional[Dict[str, Any]] = None, -) -> List[TextContent]: - """Format a response containing a list of items. - - Args: - operation: Name of the operation - items: List of items to return - item_type: Type of items (for summary message) - summary_template: Optional custom summary (use {count} and {type} placeholders) - suggestions: Optional suggestions - metadata: Optional metadata - - Returns: - Formatted response - """ - - count = len(items) - - if summary_template: - summary = summary_template.format(count=count, type=item_type) - else: - summary = f"✅ Found {count} {item_type}" - - return format_success( - operation=operation, - summary=summary, - data={ - "count": count, - item_type: items - }, - suggestions=suggestions, - metadata=metadata - ) diff --git a/pyTigerGraph/mcp/server.py b/pyTigerGraph/mcp/server.py deleted file mode 100644 index 21ed936c..00000000 --- a/pyTigerGraph/mcp/server.py +++ /dev/null @@ -1,273 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""MCP Server implementation for TigerGraph.""" - -import logging -from typing import Dict, List -from mcp.server import Server -from mcp.server.stdio import stdio_server -from mcp.types import Tool, TextContent - -from .tool_names import TigerGraphToolName -from pyTigerGraph.common.exception import TigerGraphException -from .tools import ( - get_all_tools, - # Global schema operations (database level) - get_global_schema, - # Graph operations (database level) - list_graphs, - create_graph, - drop_graph, - clear_graph_data, - # Schema operations (graph level) - get_graph_schema, - show_graph_details, - # Node tools - add_node, - add_nodes, - get_node, - get_nodes, - delete_node, - delete_nodes, - has_node, - get_node_edges, - # Edge tools - add_edge, - add_edges, - get_edge, - get_edges, - delete_edge, - delete_edges, - has_edge, - # Query tools - run_query, - run_installed_query, - install_query, - drop_query, - show_query, - get_query_metadata, - is_query_installed, - get_neighbors, - # Loading job tools - create_loading_job, - run_loading_job_with_file, - run_loading_job_with_data, - get_loading_jobs, - get_loading_job_status, - drop_loading_job, - # Statistics tools - get_vertex_count, - get_edge_count, - get_node_degree, - # GSQL tools - gsql, - generate_gsql, - generate_cypher, - # Vector schema tools - add_vector_attribute, - drop_vector_attribute, - list_vector_attributes, - get_vector_index_status, - # Vector data tools - upsert_vectors, - load_vectors_from_csv, - load_vectors_from_json, - search_top_k_similarity, - fetch_vector, - # Data Source tools - create_data_source, - update_data_source, - get_data_source, - drop_data_source, - get_all_data_sources, - drop_all_data_sources, - preview_sample_data, - # Discovery tools - discover_tools, - get_workflow, - get_tool_info, -) - -logger = logging.getLogger(__name__) - - -class MCPServer: - """MCP Server for TigerGraph.""" - - def __init__(self, name: str = "TigerGraph-MCP"): - """Initialize the MCP server.""" - self.server = Server(name) - self._setup_handlers() - - def _setup_handlers(self): - """Setup MCP server handlers.""" - - @self.server.list_tools() - async def list_tools() -> List[Tool]: - """List all available tools.""" - return get_all_tools() - - @self.server.call_tool() - async def call_tool(name: str, arguments: Dict) -> List[TextContent]: - """Handle tool calls.""" - try: - match name: - # Global schema operations (database level) - case TigerGraphToolName.GET_GLOBAL_SCHEMA: - return await get_global_schema(**arguments) - # Graph operations (database level) - case TigerGraphToolName.LIST_GRAPHS: - return await list_graphs(**arguments) - case TigerGraphToolName.CREATE_GRAPH: - return await create_graph(**arguments) - case TigerGraphToolName.DROP_GRAPH: - return await drop_graph(**arguments) - case TigerGraphToolName.CLEAR_GRAPH_DATA: - return await clear_graph_data(**arguments) - # Schema operations (graph level) - case TigerGraphToolName.GET_GRAPH_SCHEMA: - return await get_graph_schema(**arguments) - case TigerGraphToolName.SHOW_GRAPH_DETAILS: - return await show_graph_details(**arguments) - # Node operations - case TigerGraphToolName.ADD_NODE: - return await add_node(**arguments) - case TigerGraphToolName.ADD_NODES: - return await add_nodes(**arguments) - case TigerGraphToolName.GET_NODE: - return await get_node(**arguments) - case TigerGraphToolName.GET_NODES: - return await get_nodes(**arguments) - case TigerGraphToolName.DELETE_NODE: - return await delete_node(**arguments) - case TigerGraphToolName.DELETE_NODES: - return await delete_nodes(**arguments) - case TigerGraphToolName.HAS_NODE: - return await has_node(**arguments) - case TigerGraphToolName.GET_NODE_EDGES: - return await get_node_edges(**arguments) - # Edge operations - case TigerGraphToolName.ADD_EDGE: - return await add_edge(**arguments) - case TigerGraphToolName.ADD_EDGES: - return await add_edges(**arguments) - case TigerGraphToolName.GET_EDGE: - return await get_edge(**arguments) - case TigerGraphToolName.GET_EDGES: - return await get_edges(**arguments) - case TigerGraphToolName.DELETE_EDGE: - return await delete_edge(**arguments) - case TigerGraphToolName.DELETE_EDGES: - return await delete_edges(**arguments) - case TigerGraphToolName.HAS_EDGE: - return await has_edge(**arguments) - # Query operations - case TigerGraphToolName.RUN_QUERY: - return await run_query(**arguments) - case TigerGraphToolName.RUN_INSTALLED_QUERY: - return await run_installed_query(**arguments) - case TigerGraphToolName.INSTALL_QUERY: - return await install_query(**arguments) - case TigerGraphToolName.DROP_QUERY: - return await drop_query(**arguments) - case TigerGraphToolName.SHOW_QUERY: - return await show_query(**arguments) - case TigerGraphToolName.GET_QUERY_METADATA: - return await get_query_metadata(**arguments) - case TigerGraphToolName.IS_QUERY_INSTALLED: - return await is_query_installed(**arguments) - case TigerGraphToolName.GET_NEIGHBORS: - return await get_neighbors(**arguments) - # Loading job operations - case TigerGraphToolName.CREATE_LOADING_JOB: - return await create_loading_job(**arguments) - case TigerGraphToolName.RUN_LOADING_JOB_WITH_FILE: - return await run_loading_job_with_file(**arguments) - case TigerGraphToolName.RUN_LOADING_JOB_WITH_DATA: - return await run_loading_job_with_data(**arguments) - case TigerGraphToolName.GET_LOADING_JOBS: - return await get_loading_jobs(**arguments) - case TigerGraphToolName.GET_LOADING_JOB_STATUS: - return await get_loading_job_status(**arguments) - case TigerGraphToolName.DROP_LOADING_JOB: - return await drop_loading_job(**arguments) - # Statistics operations - case TigerGraphToolName.GET_VERTEX_COUNT: - return await get_vertex_count(**arguments) - case TigerGraphToolName.GET_EDGE_COUNT: - return await get_edge_count(**arguments) - case TigerGraphToolName.GET_NODE_DEGREE: - return await get_node_degree(**arguments) - # GSQL operations - case TigerGraphToolName.GSQL: - return await gsql(**arguments) - case TigerGraphToolName.GENERATE_GSQL: - return await generate_gsql(**arguments) - case TigerGraphToolName.GENERATE_CYPHER: - return await generate_cypher(**arguments) - # Vector schema operations - case TigerGraphToolName.ADD_VECTOR_ATTRIBUTE: - return await add_vector_attribute(**arguments) - case TigerGraphToolName.DROP_VECTOR_ATTRIBUTE: - return await drop_vector_attribute(**arguments) - case TigerGraphToolName.LIST_VECTOR_ATTRIBUTES: - return await list_vector_attributes(**arguments) - case TigerGraphToolName.GET_VECTOR_INDEX_STATUS: - return await get_vector_index_status(**arguments) - # Vector data operations - case TigerGraphToolName.UPSERT_VECTORS: - return await upsert_vectors(**arguments) - case TigerGraphToolName.LOAD_VECTORS_FROM_CSV: - return await load_vectors_from_csv(**arguments) - case TigerGraphToolName.LOAD_VECTORS_FROM_JSON: - return await load_vectors_from_json(**arguments) - case TigerGraphToolName.SEARCH_TOP_K_SIMILARITY: - return await search_top_k_similarity(**arguments) - case TigerGraphToolName.FETCH_VECTOR: - return await fetch_vector(**arguments) - # Data Source operations - case TigerGraphToolName.CREATE_DATA_SOURCE: - return await create_data_source(**arguments) - case TigerGraphToolName.UPDATE_DATA_SOURCE: - return await update_data_source(**arguments) - case TigerGraphToolName.GET_DATA_SOURCE: - return await get_data_source(**arguments) - case TigerGraphToolName.DROP_DATA_SOURCE: - return await drop_data_source(**arguments) - case TigerGraphToolName.GET_ALL_DATA_SOURCES: - return await get_all_data_sources(**arguments) - case TigerGraphToolName.DROP_ALL_DATA_SOURCES: - return await drop_all_data_sources(**arguments) - case TigerGraphToolName.PREVIEW_SAMPLE_DATA: - return await preview_sample_data(**arguments) - # Discovery operations - case TigerGraphToolName.DISCOVER_TOOLS: - return await discover_tools(**arguments) - case TigerGraphToolName.GET_WORKFLOW: - return await get_workflow(**arguments) - case TigerGraphToolName.GET_TOOL_INFO: - return await get_tool_info(**arguments) - case _: - raise ValueError(f"Unknown tool: {name}") - except TigerGraphException as e: - logger.exception("Error in tool execution") - error_msg = e.message if hasattr(e, 'message') else str(e) - error_code = f" (Code: {e.code})" if hasattr(e, 'code') and e.code else "" - return [TextContent(type="text", text=f"❌ TigerGraph Error{error_code} due to: {error_msg}")] - except Exception as e: - logger.exception("Error in tool execution") - return [TextContent(type="text", text=f"❌ Error due to: {str(e)}")] - - -async def serve() -> None: - """Serve the MCP server.""" - server = MCPServer() - options = server.server.create_initialization_options() - async with stdio_server() as (read_stream, write_stream): - await server.server.run(read_stream, write_stream, options, raise_exceptions=True) - diff --git a/pyTigerGraph/mcp/tool_metadata.py b/pyTigerGraph/mcp/tool_metadata.py deleted file mode 100644 index 409e5ddb..00000000 --- a/pyTigerGraph/mcp/tool_metadata.py +++ /dev/null @@ -1,528 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""Tool metadata for enhanced LLM guidance.""" - -from typing import List, Dict, Any, Optional -from pydantic import BaseModel -from enum import Enum - - -class ToolCategory(str, Enum): - """Categories for organizing tools.""" - SCHEMA = "schema" - DATA = "data" - QUERY = "query" - VECTOR = "vector" - LOADING = "loading" - DISCOVERY = "discovery" - UTILITY = "utility" - - -class ToolMetadata(BaseModel): - """Enhanced metadata for tools to help LLMs understand usage patterns.""" - category: ToolCategory - prerequisites: List[str] = [] - related_tools: List[str] = [] - common_next_steps: List[str] = [] - use_cases: List[str] = [] - complexity: str = "basic" # basic, intermediate, advanced - examples: List[Dict[str, Any]] = [] - keywords: List[str] = [] # For discovery - - -# Define metadata for each tool -TOOL_METADATA: Dict[str, ToolMetadata] = { - # Schema Operations - "tigergraph__show_graph_details": ToolMetadata( - category=ToolCategory.SCHEMA, - prerequisites=[], - related_tools=["tigergraph__get_graph_schema"], - common_next_steps=["tigergraph__add_node", "tigergraph__add_edge", "tigergraph__run_query"], - use_cases=[ - "Getting a full listing of a graph (schema, queries, jobs)", - "Understanding the structure of a graph before writing queries", - "Discovering available vertex and edge types", - "First step in any graph interaction workflow" - ], - complexity="basic", - keywords=["schema", "structure", "show", "understand", "explore", "queries", "jobs"], - examples=[ - { - "description": "Show everything under default graph", - "parameters": {} - }, - { - "description": "Show everything under a specific graph", - "parameters": {"graph_name": "SocialGraph"} - } - ] - ), - - "tigergraph__list_graphs": ToolMetadata( - category=ToolCategory.SCHEMA, - prerequisites=[], - related_tools=["tigergraph__show_graph_details", "tigergraph__create_graph"], - common_next_steps=["tigergraph__show_graph_details"], - use_cases=[ - "Discovering what graphs exist in the database", - "First step when connecting to a new TigerGraph instance", - "Verifying a graph was created successfully" - ], - complexity="basic", - keywords=["list", "graphs", "discover", "available"], - examples=[{"description": "List all graphs", "parameters": {}}] - ), - - "tigergraph__create_graph": ToolMetadata( - category=ToolCategory.SCHEMA, - prerequisites=[], - related_tools=["tigergraph__list_graphs", "tigergraph__show_graph_details"], - common_next_steps=["tigergraph__show_graph_details", "tigergraph__add_node"], - use_cases=[ - "Creating a new graph from scratch", - "Setting up a graph with specific vertex and edge types", - "Initializing a new project or data model" - ], - complexity="intermediate", - keywords=["create", "new", "graph", "initialize", "setup"], - examples=[ - { - "description": "Create a social network graph", - "parameters": { - "graph_name": "SocialGraph", - "vertex_types": [ - { - "name": "Person", - "attributes": [ - {"name": "name", "type": "STRING"}, - {"name": "age", "type": "INT"} - ] - } - ], - "edge_types": [ - { - "name": "FOLLOWS", - "from_vertex": "Person", - "to_vertex": "Person" - } - ] - } - } - ] - ), - - "tigergraph__get_graph_schema": ToolMetadata( - category=ToolCategory.SCHEMA, - prerequisites=[], - related_tools=["tigergraph__show_graph_details"], - common_next_steps=["tigergraph__add_node", "tigergraph__run_query"], - use_cases=[ - "Getting raw JSON schema for programmatic processing", - "Detailed schema inspection for advanced use cases" - ], - complexity="intermediate", - keywords=["schema", "json", "raw", "detailed"], - examples=[{"description": "Get raw schema", "parameters": {}}] - ), - - # Node Operations - "tigergraph__add_node": ToolMetadata( - category=ToolCategory.DATA, - prerequisites=["tigergraph__show_graph_details"], - related_tools=["tigergraph__add_nodes", "tigergraph__get_node", "tigergraph__delete_node"], - common_next_steps=["tigergraph__get_node", "tigergraph__add_edge", "tigergraph__get_node_edges"], - use_cases=[ - "Creating a single vertex in the graph", - "Updating an existing vertex's attributes", - "Adding individual entities (users, products, etc.)" - ], - complexity="basic", - keywords=["add", "create", "insert", "node", "vertex", "single"], - examples=[ - { - "description": "Add a person node", - "parameters": { - "vertex_type": "Person", - "vertex_id": "user123", - "attributes": {"name": "Alice", "age": 30, "city": "San Francisco"} - } - }, - { - "description": "Add a product node", - "parameters": { - "vertex_type": "Product", - "vertex_id": "prod456", - "attributes": {"name": "Laptop", "price": 999.99, "category": "Electronics"} - } - } - ] - ), - - "tigergraph__add_nodes": ToolMetadata( - category=ToolCategory.DATA, - prerequisites=["tigergraph__show_graph_details"], - related_tools=["tigergraph__add_node", "tigergraph__get_nodes"], - common_next_steps=["tigergraph__get_vertex_count", "tigergraph__add_edges"], - use_cases=[ - "Batch loading multiple vertices efficiently", - "Importing data from CSV or JSON", - "Initial data population" - ], - complexity="basic", - keywords=["add", "create", "insert", "batch", "multiple", "bulk", "nodes", "vertices"], - examples=[ - { - "description": "Add multiple person nodes", - "parameters": { - "vertex_type": "Person", - "vertices": [ - {"id": "user1", "name": "Alice", "age": 30}, - {"id": "user2", "name": "Bob", "age": 25}, - {"id": "user3", "name": "Carol", "age": 35} - ] - } - } - ] - ), - - "tigergraph__get_node": ToolMetadata( - category=ToolCategory.DATA, - prerequisites=[], - related_tools=["tigergraph__get_nodes", "tigergraph__has_node"], - common_next_steps=["tigergraph__get_node_edges", "tigergraph__delete_node"], - use_cases=[ - "Retrieving a specific vertex by ID", - "Verifying a vertex was created", - "Checking vertex attributes" - ], - complexity="basic", - keywords=["get", "retrieve", "fetch", "read", "node", "vertex", "single"], - examples=[ - { - "description": "Get a person node", - "parameters": { - "vertex_type": "Person", - "vertex_id": "user123" - } - } - ] - ), - - "tigergraph__get_nodes": ToolMetadata( - category=ToolCategory.DATA, - prerequisites=[], - related_tools=["tigergraph__get_node", "tigergraph__get_vertex_count"], - common_next_steps=["tigergraph__get_edges"], - use_cases=[ - "Retrieving multiple vertices of a type", - "Exploring graph data", - "Data export and analysis" - ], - complexity="basic", - keywords=["get", "retrieve", "fetch", "list", "multiple", "nodes", "vertices"], - examples=[ - { - "description": "Get all person nodes (limited)", - "parameters": { - "vertex_type": "Person", - "limit": 100 - } - } - ] - ), - - # Edge Operations - "tigergraph__add_edge": ToolMetadata( - category=ToolCategory.DATA, - prerequisites=["tigergraph__add_node", "tigergraph__show_graph_details"], - related_tools=["tigergraph__add_edges", "tigergraph__get_edge"], - common_next_steps=["tigergraph__get_node_edges", "tigergraph__get_neighbors"], - use_cases=[ - "Creating a relationship between two vertices", - "Connecting entities in the graph", - "Building graph structure" - ], - complexity="basic", - keywords=["add", "create", "connect", "relationship", "edge", "link"], - examples=[ - { - "description": "Create a friendship edge", - "parameters": { - "edge_type": "FOLLOWS", - "from_vertex_type": "Person", - "from_vertex_id": "user1", - "to_vertex_type": "Person", - "to_vertex_id": "user2", - "attributes": {"since": "2024-01-15"} - } - } - ] - ), - - "tigergraph__add_edges": ToolMetadata( - category=ToolCategory.DATA, - prerequisites=["tigergraph__add_nodes", "tigergraph__show_graph_details"], - related_tools=["tigergraph__add_edge"], - common_next_steps=["tigergraph__get_edge_count"], - use_cases=[ - "Batch loading multiple edges", - "Building graph structure efficiently", - "Importing relationship data" - ], - complexity="basic", - keywords=["add", "create", "batch", "multiple", "edges", "relationships", "bulk"], - examples=[] - ), - - # Query Operations - "tigergraph__run_query": ToolMetadata( - category=ToolCategory.QUERY, - prerequisites=["tigergraph__show_graph_details"], - related_tools=["tigergraph__run_installed_query", "tigergraph__get_neighbors"], - common_next_steps=[], - use_cases=[ - "Ad-hoc querying without installing", - "Testing queries before installation", - "Simple data retrieval operations", - "Running openCypher or GSQL queries" - ], - complexity="intermediate", - keywords=["query", "search", "find", "select", "interpret", "gsql", "cypher"], - examples=[ - { - "description": "Simple GSQL query", - "parameters": { - "query_text": "INTERPRET QUERY () FOR GRAPH MyGraph { SELECT v FROM Person:v LIMIT 5; PRINT v; }" - } - }, - { - "description": "openCypher query", - "parameters": { - "query_text": "INTERPRET OPENCYPHER QUERY () FOR GRAPH MyGraph { MATCH (n:Person) RETURN n LIMIT 5 }" - } - } - ] - ), - - "tigergraph__get_neighbors": ToolMetadata( - category=ToolCategory.QUERY, - prerequisites=[], - related_tools=["tigergraph__get_node_edges", "tigergraph__run_query"], - common_next_steps=[], - use_cases=[ - "Finding vertices connected to a given vertex", - "1-hop graph traversal", - "Discovering relationships" - ], - complexity="basic", - keywords=["neighbors", "connected", "adjacent", "traverse", "related"], - examples=[ - { - "description": "Get friends of a person", - "parameters": { - "vertex_type": "Person", - "vertex_id": "user1", - "edge_type": "FOLLOWS" - } - } - ] - ), - - # Vector Operations - "tigergraph__add_vector_attribute": ToolMetadata( - category=ToolCategory.VECTOR, - prerequisites=["tigergraph__show_graph_details"], - related_tools=["tigergraph__drop_vector_attribute", "tigergraph__get_vector_index_status"], - common_next_steps=["tigergraph__get_vector_index_status", "tigergraph__upsert_vectors"], - use_cases=[ - "Adding vector/embedding support to existing vertex types", - "Setting up semantic search capabilities", - "Enabling similarity-based queries" - ], - complexity="intermediate", - keywords=["vector", "embedding", "add", "attribute", "similarity", "semantic"], - examples=[ - { - "description": "Add embedding attribute for documents", - "parameters": { - "vertex_type": "Document", - "vector_name": "embedding", - "dimension": 384, - "metric": "COSINE" - } - }, - { - "description": "Add embedding for products (higher dimension)", - "parameters": { - "vertex_type": "Product", - "vector_name": "feature_vector", - "dimension": 1536, - "metric": "L2" - } - } - ] - ), - - "tigergraph__upsert_vectors": ToolMetadata( - category=ToolCategory.VECTOR, - prerequisites=["tigergraph__add_vector_attribute", "tigergraph__get_vector_index_status"], - related_tools=["tigergraph__search_top_k_similarity", "tigergraph__fetch_vector"], - common_next_steps=["tigergraph__get_vector_index_status", "tigergraph__search_top_k_similarity"], - use_cases=[ - "Loading embedding vectors into the graph", - "Updating vector data for vertices", - "Populating semantic search index" - ], - complexity="intermediate", - keywords=["vector", "embedding", "upsert", "load", "insert", "update"], - examples=[ - { - "description": "Upsert document embeddings", - "parameters": { - "vertex_type": "Document", - "vector_attribute": "embedding", - "vectors": [ - { - "vertex_id": "doc1", - "vector": [0.1, 0.2, 0.3], - "attributes": {"title": "Document 1"} - } - ] - } - } - ] - ), - - "tigergraph__search_top_k_similarity": ToolMetadata( - category=ToolCategory.VECTOR, - prerequisites=["tigergraph__upsert_vectors", "tigergraph__get_vector_index_status"], - related_tools=["tigergraph__fetch_vector"], - common_next_steps=[], - use_cases=[ - "Finding similar documents or items", - "Semantic search operations", - "Recommendation based on similarity" - ], - complexity="intermediate", - keywords=["vector", "search", "similarity", "nearest", "semantic", "find", "similar"], - examples=[ - { - "description": "Find similar documents", - "parameters": { - "vertex_type": "Document", - "vector_attribute": "embedding", - "query_vector": [0.1, 0.2, 0.3], - "top_k": 10 - } - } - ] - ), - - # Loading Operations - "tigergraph__create_loading_job": ToolMetadata( - category=ToolCategory.LOADING, - prerequisites=["tigergraph__show_graph_details"], - related_tools=["tigergraph__run_loading_job_with_file", "tigergraph__run_loading_job_with_data"], - common_next_steps=["tigergraph__run_loading_job_with_file", "tigergraph__get_loading_jobs"], - use_cases=[ - "Setting up data ingestion from CSV/JSON files", - "Defining how file columns map to vertex/edge attributes", - "Preparing for bulk data loading" - ], - complexity="advanced", - keywords=["loading", "job", "create", "define", "ingest", "import"], - examples=[] - ), - - "tigergraph__run_loading_job_with_file": ToolMetadata( - category=ToolCategory.LOADING, - prerequisites=["tigergraph__create_loading_job"], - related_tools=["tigergraph__run_loading_job_with_data", "tigergraph__get_loading_job_status"], - common_next_steps=["tigergraph__get_loading_job_status", "tigergraph__get_vertex_count"], - use_cases=[ - "Loading data from CSV or JSON files", - "Bulk import of graph data", - "ETL operations" - ], - complexity="intermediate", - keywords=["loading", "job", "run", "file", "import", "bulk"], - examples=[] - ), - - # Statistics - "tigergraph__get_vertex_count": ToolMetadata( - category=ToolCategory.UTILITY, - prerequisites=[], - related_tools=["tigergraph__get_edge_count", "tigergraph__get_nodes"], - common_next_steps=[], - use_cases=[ - "Verifying data was loaded", - "Monitoring graph size", - "Data validation" - ], - complexity="basic", - keywords=["count", "statistics", "size", "vertex", "node", "total"], - examples=[ - { - "description": "Count all vertices", - "parameters": {} - }, - { - "description": "Count specific vertex type", - "parameters": {"vertex_type": "Person"} - } - ] - ), - - "tigergraph__get_edge_count": ToolMetadata( - category=ToolCategory.UTILITY, - prerequisites=[], - related_tools=["tigergraph__get_vertex_count"], - common_next_steps=[], - use_cases=[ - "Verifying relationships were created", - "Monitoring graph connectivity", - "Data validation" - ], - complexity="basic", - keywords=["count", "statistics", "size", "edge", "relationship", "total"], - examples=[] - ), -} - - -def get_tool_metadata(tool_name: str) -> Optional[ToolMetadata]: - """Get metadata for a specific tool.""" - return TOOL_METADATA.get(tool_name) - - -def get_tools_by_category(category: ToolCategory) -> List[str]: - """Get all tool names in a specific category.""" - return [ - tool_name for tool_name, metadata in TOOL_METADATA.items() - if metadata.category == category - ] - - -def search_tools_by_keywords(keywords: List[str]) -> List[str]: - """Search for tools matching any of the provided keywords.""" - matching_tools = [] - keywords_lower = [k.lower() for k in keywords] - - for tool_name, metadata in TOOL_METADATA.items(): - # Check if any keyword matches - for keyword in keywords_lower: - if any(keyword in mk.lower() for mk in metadata.keywords): - matching_tools.append(tool_name) - break - # Also check in use cases - if any(keyword in uc.lower() for uc in metadata.use_cases): - matching_tools.append(tool_name) - break - - return matching_tools diff --git a/pyTigerGraph/mcp/tool_names.py b/pyTigerGraph/mcp/tool_names.py deleted file mode 100644 index 2ca7ee81..00000000 --- a/pyTigerGraph/mcp/tool_names.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""Tool names for TigerGraph MCP tools.""" - -from enum import Enum - - -class TigerGraphToolName(str, Enum): - """Enumeration of all available TigerGraph MCP tool names.""" - - # Global Schema Operations (Database level - operates on global schema) - GET_GLOBAL_SCHEMA = "tigergraph__get_global_schema" - - # Graph Operations (Database level - operates on graphs within the database) - LIST_GRAPHS = "tigergraph__list_graphs" - CREATE_GRAPH = "tigergraph__create_graph" - DROP_GRAPH = "tigergraph__drop_graph" - CLEAR_GRAPH_DATA = "tigergraph__clear_graph_data" - - # Schema Operations (Graph level - operates on schema within a specific graph) - GET_GRAPH_SCHEMA = "tigergraph__get_graph_schema" - SHOW_GRAPH_DETAILS = "tigergraph__show_graph_details" - - # Node Operations - ADD_NODE = "tigergraph__add_node" - ADD_NODES = "tigergraph__add_nodes" - GET_NODE = "tigergraph__get_node" - GET_NODES = "tigergraph__get_nodes" - DELETE_NODE = "tigergraph__delete_node" - DELETE_NODES = "tigergraph__delete_nodes" - HAS_NODE = "tigergraph__has_node" - GET_NODE_EDGES = "tigergraph__get_node_edges" - - # Edge Operations - ADD_EDGE = "tigergraph__add_edge" - ADD_EDGES = "tigergraph__add_edges" - GET_EDGE = "tigergraph__get_edge" - GET_EDGES = "tigergraph__get_edges" - DELETE_EDGE = "tigergraph__delete_edge" - DELETE_EDGES = "tigergraph__delete_edges" - HAS_EDGE = "tigergraph__has_edge" - - # Query Operations - RUN_QUERY = "tigergraph__run_query" - RUN_INSTALLED_QUERY = "tigergraph__run_installed_query" - INSTALL_QUERY = "tigergraph__install_query" - DROP_QUERY = "tigergraph__drop_query" - SHOW_QUERY = "tigergraph__show_query" - GET_QUERY_METADATA = "tigergraph__get_query_metadata" - IS_QUERY_INSTALLED = "tigergraph__is_query_installed" - GET_NEIGHBORS = "tigergraph__get_neighbors" - - # Loading Job Operations - CREATE_LOADING_JOB = "tigergraph__create_loading_job" - RUN_LOADING_JOB_WITH_FILE = "tigergraph__run_loading_job_with_file" - RUN_LOADING_JOB_WITH_DATA = "tigergraph__run_loading_job_with_data" - GET_LOADING_JOBS = "tigergraph__get_loading_jobs" - GET_LOADING_JOB_STATUS = "tigergraph__get_loading_job_status" - DROP_LOADING_JOB = "tigergraph__drop_loading_job" - - # Statistics - GET_VERTEX_COUNT = "tigergraph__get_vertex_count" - GET_EDGE_COUNT = "tigergraph__get_edge_count" - GET_NODE_DEGREE = "tigergraph__get_node_degree" - - # GSQL Operations - GSQL = "tigergraph__gsql" - GENERATE_GSQL = "tigergraph__generate_gsql" - GENERATE_CYPHER = "tigergraph__generate_cypher" - - # Vector Schema Operations - ADD_VECTOR_ATTRIBUTE = "tigergraph__add_vector_attribute" - DROP_VECTOR_ATTRIBUTE = "tigergraph__drop_vector_attribute" - LIST_VECTOR_ATTRIBUTES = "tigergraph__list_vector_attributes" - GET_VECTOR_INDEX_STATUS = "tigergraph__get_vector_index_status" - - # Vector Data Operations - UPSERT_VECTORS = "tigergraph__upsert_vectors" - LOAD_VECTORS_FROM_CSV = "tigergraph__load_vectors_from_csv" - LOAD_VECTORS_FROM_JSON = "tigergraph__load_vectors_from_json" - SEARCH_TOP_K_SIMILARITY = "tigergraph__search_top_k_similarity" - FETCH_VECTOR = "tigergraph__fetch_vector" - - # Data Source Operations - CREATE_DATA_SOURCE = "tigergraph__create_data_source" - UPDATE_DATA_SOURCE = "tigergraph__update_data_source" - GET_DATA_SOURCE = "tigergraph__get_data_source" - DROP_DATA_SOURCE = "tigergraph__drop_data_source" - GET_ALL_DATA_SOURCES = "tigergraph__get_all_data_sources" - DROP_ALL_DATA_SOURCES = "tigergraph__drop_all_data_sources" - PREVIEW_SAMPLE_DATA = "tigergraph__preview_sample_data" - - # Discovery and Navigation Operations - DISCOVER_TOOLS = "tigergraph__discover_tools" - GET_WORKFLOW = "tigergraph__get_workflow" - GET_TOOL_INFO = "tigergraph__get_tool_info" - - @classmethod - def from_value(cls, value: str) -> "TigerGraphToolName": - """Get enum from string value.""" - for tool in cls: - if tool.value == value: - return tool - raise ValueError(f"Unknown tool name: {value}") - diff --git a/pyTigerGraph/mcp/tools/__init__.py b/pyTigerGraph/mcp/tools/__init__.py deleted file mode 100644 index 3922142e..00000000 --- a/pyTigerGraph/mcp/tools/__init__.py +++ /dev/null @@ -1,298 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""MCP tools for TigerGraph.""" - -from .schema_tools import ( - # Global schema operations (database level) - get_global_schema_tool, - get_global_schema, - # Graph operations (database level) - list_graphs_tool, - create_graph_tool, - drop_graph_tool, - clear_graph_data_tool, - list_graphs, - create_graph, - drop_graph, - clear_graph_data, - # Schema operations (graph level) - get_graph_schema_tool, - show_graph_details_tool, - get_graph_schema, - show_graph_details, -) -from .node_tools import ( - add_node_tool, - add_nodes_tool, - get_node_tool, - get_nodes_tool, - delete_node_tool, - delete_nodes_tool, - has_node_tool, - get_node_edges_tool, - add_node, - add_nodes, - get_node, - get_nodes, - delete_node, - delete_nodes, - has_node, - get_node_edges, -) -from .edge_tools import ( - add_edge_tool, - add_edges_tool, - get_edge_tool, - get_edges_tool, - delete_edge_tool, - delete_edges_tool, - has_edge_tool, - add_edge, - add_edges, - get_edge, - get_edges, - delete_edge, - delete_edges, - has_edge, -) -from .query_tools import ( - run_query_tool, - run_installed_query_tool, - install_query_tool, - drop_query_tool, - show_query_tool, - get_query_metadata_tool, - is_query_installed_tool, - get_neighbors_tool, - run_query, - run_installed_query, - install_query, - drop_query, - show_query, - get_query_metadata, - is_query_installed, - get_neighbors, -) -from .data_tools import ( - create_loading_job_tool, - run_loading_job_with_file_tool, - run_loading_job_with_data_tool, - get_loading_jobs_tool, - get_loading_job_status_tool, - drop_loading_job_tool, - create_loading_job, - run_loading_job_with_file, - run_loading_job_with_data, - get_loading_jobs, - get_loading_job_status, - drop_loading_job, -) -from .statistics_tools import ( - get_vertex_count_tool, - get_edge_count_tool, - get_node_degree_tool, - get_vertex_count, - get_edge_count, - get_node_degree, -) -from .gsql_tools import ( - gsql_tool, - gsql, - generate_gsql_query_tool, - generate_gsql, - generate_cypher_query_tool, - generate_cypher, -) -from .vector_tools import ( - # Vector schema tools - add_vector_attribute_tool, - drop_vector_attribute_tool, - list_vector_attributes_tool, - get_vector_index_status_tool, - add_vector_attribute, - drop_vector_attribute, - list_vector_attributes, - get_vector_index_status, - # Vector data tools - upsert_vectors_tool, - load_vectors_from_csv_tool, - load_vectors_from_json_tool, - search_top_k_similarity_tool, - fetch_vector_tool, - upsert_vectors, - load_vectors_from_csv, - load_vectors_from_json, - search_top_k_similarity, - fetch_vector, -) -from .datasource_tools import ( - create_data_source_tool, - update_data_source_tool, - get_data_source_tool, - drop_data_source_tool, - get_all_data_sources_tool, - drop_all_data_sources_tool, - preview_sample_data_tool, - create_data_source, - update_data_source, - get_data_source, - drop_data_source, - get_all_data_sources, - drop_all_data_sources, - preview_sample_data, -) -from .discovery_tools import ( - discover_tools_tool, - get_workflow_tool, - get_tool_info_tool, - discover_tools, - get_workflow, - get_tool_info, -) -from .tool_registry import get_all_tools - -__all__ = [ - # Global schema operations (database level) - "get_global_schema_tool", - "get_global_schema", - # Graph operations (database level) - "list_graphs_tool", - "create_graph_tool", - "drop_graph_tool", - "clear_graph_data_tool", - "list_graphs", - "create_graph", - "drop_graph", - "clear_graph_data", - # Schema operations (graph level) - "get_graph_schema_tool", - "show_graph_details_tool", - "get_graph_schema", - "show_graph_details", - # Node tools - "add_node_tool", - "add_nodes_tool", - "get_node_tool", - "get_nodes_tool", - "delete_node_tool", - "delete_nodes_tool", - "has_node_tool", - "get_node_edges_tool", - "add_node", - "add_nodes", - "get_node", - "get_nodes", - "delete_node", - "delete_nodes", - "has_node", - "get_node_edges", - # Edge tools - "add_edge_tool", - "add_edges_tool", - "get_edge_tool", - "get_edges_tool", - "delete_edge_tool", - "delete_edges_tool", - "has_edge_tool", - "add_edge", - "add_edges", - "get_edge", - "get_edges", - "delete_edge", - "delete_edges", - "has_edge", - # Query tools - "run_query_tool", - "run_installed_query_tool", - "install_query_tool", - "drop_query_tool", - "show_query_tool", - "get_query_metadata_tool", - "is_query_installed_tool", - "get_neighbors_tool", - "run_query", - "run_installed_query", - "install_query", - "drop_query", - "show_query", - "get_query_metadata", - "is_query_installed", - "get_neighbors", - # Loading job tools - "create_loading_job_tool", - "run_loading_job_with_file_tool", - "run_loading_job_with_data_tool", - "get_loading_jobs_tool", - "get_loading_job_status_tool", - "drop_loading_job_tool", - "create_loading_job", - "run_loading_job_with_file", - "run_loading_job_with_data", - "get_loading_jobs", - "get_loading_job_status", - "drop_loading_job", - # Statistics tools - "get_vertex_count_tool", - "get_edge_count_tool", - "get_node_degree_tool", - "get_vertex_count", - "get_edge_count", - "get_node_degree", - # GSQL tools - "gsql_tool", - "gsql", - "generate_gsql_query_tool", - "generate_gsql", - "generate_cypher_query_tool", - "generate_cypher", - # Vector schema tools - "add_vector_attribute_tool", - "drop_vector_attribute_tool", - "list_vector_attributes_tool", - "get_vector_index_status_tool", - "add_vector_attribute", - "drop_vector_attribute", - "list_vector_attributes", - "get_vector_index_status", - # Vector data tools - "upsert_vectors_tool", - "load_vectors_from_csv_tool", - "load_vectors_from_json_tool", - "search_top_k_similarity_tool", - "fetch_vector_tool", - "upsert_vectors", - "load_vectors_from_csv", - "load_vectors_from_json", - "search_top_k_similarity", - "fetch_vector", - # Data Source tools - "create_data_source_tool", - "update_data_source_tool", - "get_data_source_tool", - "drop_data_source_tool", - "get_all_data_sources_tool", - "drop_all_data_sources_tool", - "preview_sample_data_tool", - "create_data_source", - "update_data_source", - "get_data_source", - "drop_data_source", - "get_all_data_sources", - "drop_all_data_sources", - "preview_sample_data", - # Discovery tools - "discover_tools_tool", - "get_workflow_tool", - "get_tool_info_tool", - "discover_tools", - "get_workflow", - "get_tool_info", - # Registry - "get_all_tools", -] - diff --git a/pyTigerGraph/mcp/tools/data_tools.py b/pyTigerGraph/mcp/tools/data_tools.py deleted file mode 100644 index 77b589c1..00000000 --- a/pyTigerGraph/mcp/tools/data_tools.py +++ /dev/null @@ -1,626 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""Data loading tools for MCP. - -These tools use the non-deprecated loading job APIs: -- createLoadingJob - Create a loading job from structured config or GSQL -- runLoadingJobWithFile - Execute loading job with a file -- runLoadingJobWithData - Execute loading job with data string -- getLoadingJobs - List all loading jobs -- getLoadingJobStatus - Get status of a loading job -- dropLoadingJob - Drop a loading job -""" - -import json -from typing import List, Optional, Dict, Any, Union -from pydantic import BaseModel, Field -from mcp.types import Tool, TextContent - -from ..tool_names import TigerGraphToolName -from ..connection_manager import get_connection -from ..response_formatter import format_success, format_error, gsql_has_error -from pyTigerGraph.common.exception import TigerGraphException - - -# ============================================================================= -# Input Models for Loading Job Configuration -# ============================================================================= - -class NodeMapping(BaseModel): - """Mapping configuration for loading vertices.""" - vertex_type: str = Field(..., description="Target vertex type name.") - attribute_mappings: Dict[str, Union[str, int]] = Field( - ..., - description="Map of attribute name to column index (int) or header name (string). Must include the primary key. Example: {'id': 0, 'name': 1} or {'id': 'user_id', 'name': 'user_name'}" - ) - - -class EdgeMapping(BaseModel): - """Mapping configuration for loading edges.""" - edge_type: str = Field(..., description="Target edge type name.") - source_column: Union[str, int] = Field(..., description="Column for source vertex ID (string for header name, int for column index).") - target_column: Union[str, int] = Field(..., description="Column for target vertex ID (string for header name, int for column index).") - attribute_mappings: Optional[Dict[str, Union[str, int]]] = Field( - default_factory=dict, - description="Map of attribute name to column. Optional for edges without attributes." - ) - - -class FileConfig(BaseModel): - """Configuration for a single data file in a loading job.""" - file_alias: str = Field(..., description="Alias for the file (used in DEFINE FILENAME).") - file_path: Optional[str] = Field(None, description="Path to the file. If not provided, data will be passed at runtime.") - separator: str = Field(",", description="Field separator character.") - header: str = Field("true", description="Whether the file has a header row ('true' or 'false').") - eol: str = Field("\\n", description="End-of-line character.") - quote: Optional[str] = Field(None, description="Quote character for CSV (e.g., 'DOUBLE' for double quotes).") - node_mappings: List[NodeMapping] = Field( - default_factory=list, - description="List of vertex loading mappings. Example: [{'vertex_type': 'Person', 'attribute_mappings': {'id': 0, 'name': 1}}]" - ) - edge_mappings: List[EdgeMapping] = Field( - default_factory=list, - description="List of edge loading mappings." - ) - - -class CreateLoadingJobToolInput(BaseModel): - """Input schema for creating a loading job.""" - graph_name: Optional[str] = Field(None, description="Name of the graph. If not provided, uses default connection.") - job_name: str = Field(..., description="Name for the loading job.") - files: List[FileConfig] = Field( - ..., - description="List of file configurations. Each file must have a 'file_alias' and 'node_mappings' and/or 'edge_mappings'. Example: [{'file_alias': 'f1', 'node_mappings': [...]}]" - ) - run_job: bool = Field(False, description="If True, run the loading job immediately after creation.") - drop_after_run: bool = Field(False, description="If True, drop the job after running (only applies if run_job=True).") - - -# ============================================================================= -# Input Models for Other Operations -# ============================================================================= - -class RunLoadingJobWithFileToolInput(BaseModel): - """Input schema for running a loading job with a file.""" - graph_name: Optional[str] = Field(None, description="Name of the graph. If not provided, uses default connection.") - file_path: str = Field(..., description="Absolute path to the data file to load. Example: '/home/user/data/persons.csv'") - file_tag: str = Field(..., description="The name of file variable in the loading job (DEFINE FILENAME ).") - job_name: str = Field(..., description="The name of the loading job to run.") - separator: Optional[str] = Field(None, description="Data value separator. Default is comma. For JSON data, don't specify.") - eol: Optional[str] = Field(None, description="End-of-line character. Default is '\\n'. Supports '\\r\\n'.") - timeout: int = Field(16000, description="Timeout in milliseconds. Set to 0 for system-wide timeout.") - size_limit: int = Field(128000000, description="Maximum size for input file in bytes (default 128MB).") - - -class RunLoadingJobWithDataToolInput(BaseModel): - """Input schema for running a loading job with inline data.""" - graph_name: Optional[str] = Field(None, description="Name of the graph. If not provided, uses default connection.") - data: str = Field(..., description="The data string to load (CSV, JSON, etc.). Example: 'user1,Alice\\nuser2,Bob'") - file_tag: str = Field(..., description="The name of file variable in the loading job (DEFINE FILENAME ).") - job_name: str = Field(..., description="The name of the loading job to run.") - separator: Optional[str] = Field(None, description="Data value separator. Default is comma. For JSON data, don't specify.") - eol: Optional[str] = Field(None, description="End-of-line character. Default is '\\n'. Supports '\\r\\n'.") - timeout: int = Field(16000, description="Timeout in milliseconds. Set to 0 for system-wide timeout.") - size_limit: int = Field(128000000, description="Maximum size for input data in bytes (default 128MB).") - - -class GetLoadingJobsToolInput(BaseModel): - """Input schema for listing loading jobs.""" - graph_name: Optional[str] = Field(None, description="Name of the graph. If not provided, uses default connection.") - - -class GetLoadingJobStatusToolInput(BaseModel): - """Input schema for getting loading job status.""" - graph_name: Optional[str] = Field(None, description="Name of the graph. If not provided, uses default connection.") - job_id: str = Field(..., description="The ID of the loading job to check status.") - - -class DropLoadingJobToolInput(BaseModel): - """Input schema for dropping a loading job.""" - graph_name: Optional[str] = Field(None, description="Name of the graph. If not provided, uses default connection.") - job_name: str = Field(..., description="The name of the loading job to drop.") - - -# ============================================================================= -# Tool Definitions -# ============================================================================= - -create_loading_job_tool = Tool( - name=TigerGraphToolName.CREATE_LOADING_JOB, - description="""Create a loading job from structured configuration. -The job defines how to load data from files into vertices and edges. -Each file config specifies: file alias, separator, header, EOL, and mappings. -Node mappings define which columns map to vertex attributes. -Edge mappings define source/target columns and edge attributes. -Optionally run the job immediately and drop it after execution.""", - inputSchema=CreateLoadingJobToolInput.model_json_schema(), -) - -run_loading_job_with_file_tool = Tool( - name=TigerGraphToolName.RUN_LOADING_JOB_WITH_FILE, - description="Execute a loading job with a data file. The file is uploaded to TigerGraph and loaded according to the specified loading job definition.", - inputSchema=RunLoadingJobWithFileToolInput.model_json_schema(), -) - -run_loading_job_with_data_tool = Tool( - name=TigerGraphToolName.RUN_LOADING_JOB_WITH_DATA, - description="Execute a loading job with inline data string. The data is posted to TigerGraph and loaded according to the specified loading job definition.", - inputSchema=RunLoadingJobWithDataToolInput.model_json_schema(), -) - -get_loading_jobs_tool = Tool( - name=TigerGraphToolName.GET_LOADING_JOBS, - description="Get a list of all loading jobs defined for the current graph.", - inputSchema=GetLoadingJobsToolInput.model_json_schema(), -) - -get_loading_job_status_tool = Tool( - name=TigerGraphToolName.GET_LOADING_JOB_STATUS, - description="Get the status of a specific loading job by its job ID.", - inputSchema=GetLoadingJobStatusToolInput.model_json_schema(), -) - -drop_loading_job_tool = Tool( - name=TigerGraphToolName.DROP_LOADING_JOB, - description="Drop (delete) a loading job from the graph.", - inputSchema=DropLoadingJobToolInput.model_json_schema(), -) - - -# ============================================================================= -# Helper Functions -# ============================================================================= - -def _format_column(column: Union[str, int]) -> str: - """Format column reference for GSQL loading job.""" - if isinstance(column, int): - return f"${column}" - return f'$"{column}"' - - -def _generate_loading_job_gsql( - graph_name: str, - job_name: str, - files: List[Dict[str, Any]], -) -> str: - """Generate GSQL script for creating a loading job.""" - - # Build DEFINE FILENAME statements - define_files = [] - for file_config in files: - alias = file_config["file_alias"] - path = file_config.get("file_path") - if path: - define_files.append(f'DEFINE FILENAME {alias} = "{path}";') - else: - define_files.append(f"DEFINE FILENAME {alias};") - - # Build LOAD statements for each file - load_statements = [] - for file_config in files: - alias = file_config["file_alias"] - separator = file_config.get("separator", ",") - header = file_config.get("header", "true") - eol = file_config.get("eol", "\\n") - quote = file_config.get("quote") - - # Build USING clause - using_parts = [ - f'SEPARATOR="{separator}"', - f'HEADER="{header}"', - f'EOL="{eol}"' - ] - if quote: - using_parts.append(f'QUOTE="{quote}"') - using_clause = "USING " + ", ".join(using_parts) + ";" - - # Build mapping statements - mapping_statements = [] - - # Node mappings - for node_mapping in file_config.get("node_mappings", []): - vertex_type = node_mapping["vertex_type"] - attr_mappings = node_mapping["attribute_mappings"] - - # Format attribute values - attr_values = ", ".join( - _format_column(col) for col in attr_mappings.values() - ) - mapping_statements.append( - f"TO VERTEX {vertex_type} VALUES({attr_values})" - ) - - # Edge mappings - for edge_mapping in file_config.get("edge_mappings", []): - edge_type = edge_mapping["edge_type"] - source_col = _format_column(edge_mapping["source_column"]) - target_col = _format_column(edge_mapping["target_column"]) - attr_mappings = edge_mapping.get("attribute_mappings", {}) - - # Format attribute values - if attr_mappings: - attr_values = ", ".join( - _format_column(col) for col in attr_mappings.values() - ) - all_values = f"{source_col}, {target_col}, {attr_values}" - else: - all_values = f"{source_col}, {target_col}" - - mapping_statements.append( - f"TO EDGE {edge_type} VALUES({all_values})" - ) - - # Combine into LOAD statement - if mapping_statements: - load_stmt = f"LOAD {alias}\n " + ",\n ".join(mapping_statements) + f"\n {using_clause}" - load_statements.append(load_stmt) - - # Build the complete GSQL script - define_section = " # Define files\n " + "\n ".join(define_files) - load_section = " # Load data\n " + "\n ".join(load_statements) - - gsql_script = f"""USE GRAPH {graph_name} - -CREATE LOADING JOB {job_name} FOR GRAPH {graph_name} {{ -{define_section} - -{load_section} -}}""" - - return gsql_script - - -# ============================================================================= -# Tool Implementations -# ============================================================================= - -async def create_loading_job( - job_name: str, - files: List[Dict[str, Any]], - run_job: bool = False, - drop_after_run: bool = False, - graph_name: Optional[str] = None, -) -> List[TextContent]: - """Create a loading job from structured configuration.""" - try: - conn = get_connection(graph_name=graph_name) - - # Generate the GSQL script - gsql_script = _generate_loading_job_gsql( - graph_name=conn.graphname, - job_name=job_name, - files=files - ) - - # Add RUN and DROP commands if requested - if run_job: - gsql_script += f"\n\nRUN LOADING JOB {job_name}" - if drop_after_run: - gsql_script += f"\n\nDROP JOB {job_name}" - - result = await conn.gsql(gsql_script) - result_str = str(result) if result else "" - - if gsql_has_error(result_str): - return format_error( - operation="create_loading_job", - error=TigerGraphException(result_str), - context={ - "job_name": job_name, - "graph_name": conn.graphname, - "gsql_script": gsql_script, - }, - suggestions=[ - "Check that vertex/edge types referenced in the job exist in the schema", - "Use show_graph_details() to verify the current schema", - "Ensure file paths and column mappings are correct", - ], - ) - - status_parts = [] - if run_job: - if drop_after_run: - status_parts.append("Job created, executed, and dropped (one-time load)") - else: - status_parts.append("Job created and executed") - else: - status_parts.append("Job created successfully") - - return format_success( - operation="create_loading_job", - summary=f"Success: Loading job '{job_name}' " + ", ".join(status_parts), - data={ - "job_name": job_name, - "file_count": len(files), - "executed": run_job, - "dropped": drop_after_run, - "gsql_script": gsql_script, - "result": result_str, - }, - suggestions=[s for s in [ - f"Run the job: run_loading_job_with_file(job_name='{job_name}', ...)" if not run_job else "Job already executed", - "List all jobs: get_loading_jobs()", - f"Get status: get_loading_job_status(job_name='{job_name}')" if not drop_after_run else None, - "Tip: Loading jobs are the recommended way to bulk-load data" - ] if s is not None], - metadata={ - "graph_name": conn.graphname, - "operation_type": "DDL" - } - ) - - except Exception as e: - return format_error( - operation="create_loading_job", - error=e, - context={ - "job_name": job_name, - "file_count": len(files), - "graph_name": graph_name or "default" - } - ) - - -async def run_loading_job_with_file( - file_path: str, - file_tag: str, - job_name: str, - separator: Optional[str] = None, - eol: Optional[str] = None, - timeout: int = 16000, - size_limit: int = 128000000, - graph_name: Optional[str] = None, -) -> List[TextContent]: - """Execute a loading job with a data file.""" - try: - conn = get_connection(graph_name=graph_name) - result = await conn.runLoadingJobWithFile( - filePath=file_path, - fileTag=file_tag, - jobName=job_name, - sep=separator, - eol=eol, - timeout=timeout, - sizeLimit=size_limit - ) - if result: - return format_success( - operation="run_loading_job_with_file", - summary=f"Success: Loading job '{job_name}' executed successfully with file '{file_path}'", - data={ - "job_name": job_name, - "file_path": file_path, - "file_tag": file_tag, - "result": result - }, - suggestions=[ - f"Check status: get_loading_job_status(job_id='')", - "Verify loaded data with: get_vertex_count() or get_edge_count()", - "List all jobs: get_loading_jobs()" - ], - metadata={"graph_name": conn.graphname} - ) - else: - return format_error( - operation="run_loading_job_with_file", - error=ValueError("Loading job returned no result"), - context={ - "job_name": job_name, - "file_path": file_path, - "file_tag": file_tag, - "graph_name": graph_name or "default" - }, - suggestions=[ - "Check if the job name is correct", - "Verify the file_tag matches the loading job definition", - "Ensure the loading job exists: get_loading_jobs()" - ] - ) - except Exception as e: - return format_error( - operation="run_loading_job_with_file", - error=e, - context={ - "job_name": job_name, - "file_path": file_path, - "graph_name": graph_name or "default" - } - ) - - -async def run_loading_job_with_data( - data: str, - file_tag: str, - job_name: str, - separator: Optional[str] = None, - eol: Optional[str] = None, - timeout: int = 16000, - size_limit: int = 128000000, - graph_name: Optional[str] = None, -) -> List[TextContent]: - """Execute a loading job with inline data string.""" - try: - conn = get_connection(graph_name=graph_name) - result = await conn.runLoadingJobWithData( - data=data, - fileTag=file_tag, - jobName=job_name, - sep=separator, - eol=eol, - timeout=timeout, - sizeLimit=size_limit - ) - if result: - data_preview = data[:100] + "..." if len(data) > 100 else data - return format_success( - operation="run_loading_job_with_data", - summary=f"Success: Loading job '{job_name}' executed successfully with inline data", - data={ - "job_name": job_name, - "file_tag": file_tag, - "data_preview": data_preview, - "data_size": len(data), - "result": result - }, - suggestions=[ - "Verify loaded data: get_vertex_count() or get_edge_count()", - "Tip: For large datasets, use 'run_loading_job_with_file' instead", - "List all jobs: get_loading_jobs()" - ], - metadata={"graph_name": conn.graphname} - ) - else: - return format_error( - operation="run_loading_job_with_data", - error=ValueError("Loading job returned no result"), - context={ - "job_name": job_name, - "file_tag": file_tag, - "data_size": len(data), - "graph_name": graph_name or "default" - }, - suggestions=[ - "Check if the job name is correct", - "Verify the file_tag matches the loading job definition", - "Ensure the loading job exists: get_loading_jobs()" - ] - ) - except Exception as e: - return format_error( - operation="run_loading_job_with_data", - error=e, - context={ - "job_name": job_name, - "data_size": len(data), - "graph_name": graph_name or "default" - } - ) - - -async def get_loading_jobs( - graph_name: Optional[str] = None, -) -> List[TextContent]: - """Get a list of all loading jobs for the current graph.""" - try: - conn = get_connection(graph_name=graph_name) - result = await conn.getLoadingJobs() - if result: - job_count = len(result) if isinstance(result, list) else 1 - return format_success( - operation="get_loading_jobs", - summary=f"Found {job_count} loading job(s) for graph '{conn.graphname}'", - data={ - "jobs": result, - "count": job_count - }, - suggestions=[ - "Run a job: run_loading_job_with_file(...) or run_loading_job_with_data(...)", - "Create new job: create_loading_job(...)", - "Check job status: get_loading_job_status(job_id='')" - ], - metadata={"graph_name": conn.graphname} - ) - else: - return format_success( - operation="get_loading_jobs", - summary=f"Success: No loading jobs found for graph '{conn.graphname}'", - suggestions=[ - "Create a loading job: create_loading_job(...)", - "Tip: Loading jobs are used for bulk data ingestion" - ], - metadata={"graph_name": conn.graphname} - ) - except Exception as e: - return format_error( - operation="get_loading_jobs", - error=e, - context={"graph_name": graph_name or "default"} - ) - - -async def get_loading_job_status( - job_id: str, - graph_name: Optional[str] = None, -) -> List[TextContent]: - """Get the status of a specific loading job.""" - try: - conn = get_connection(graph_name=graph_name) - result = await conn.getLoadingJobStatus(jobId=job_id) - if result: - return format_success( - operation="get_loading_job_status", - summary=f"Success: Loading job status for '{job_id}'", - data={ - "job_id": job_id, - "status": result - }, - suggestions=[ - "List all jobs: get_loading_jobs()", - "Tip: Use this to monitor long-running loading jobs" - ], - metadata={"graph_name": conn.graphname} - ) - else: - return format_error( - operation="get_loading_job_status", - error=ValueError("No status found for loading job"), - context={ - "job_id": job_id, - "graph_name": graph_name or "default" - }, - suggestions=[ - "Verify the job_id is correct", - "List all jobs: get_loading_jobs()" - ] - ) - except Exception as e: - return format_error( - operation="get_loading_job_status", - error=e, - context={ - "job_id": job_id, - "graph_name": graph_name or "default" - } - ) - - -async def drop_loading_job( - job_name: str, - graph_name: Optional[str] = None, -) -> List[TextContent]: - """Drop a loading job from the graph.""" - try: - conn = get_connection(graph_name=graph_name) - result = await conn.dropLoadingJob(jobName=job_name) - - return format_success( - operation="drop_loading_job", - summary=f"Success: Loading job '{job_name}' dropped successfully", - data={ - "job_name": job_name, - "result": result - }, - suggestions=[ - "Warning: This operation is permanent and cannot be undone", - "Verify deletion: get_loading_jobs()", - "Create a new job: create_loading_job(...)" - ], - metadata={ - "graph_name": conn.graphname, - "destructive": True - } - ) - except Exception as e: - return format_error( - operation="drop_loading_job", - error=e, - context={ - "job_name": job_name, - "graph_name": graph_name or "default" - } - ) diff --git a/pyTigerGraph/mcp/tools/datasource_tools.py b/pyTigerGraph/mcp/tools/datasource_tools.py deleted file mode 100644 index c8d0b46a..00000000 --- a/pyTigerGraph/mcp/tools/datasource_tools.py +++ /dev/null @@ -1,362 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""Data source operation tools for MCP.""" - -from typing import List, Optional, Dict, Any -from pydantic import BaseModel, Field -from mcp.types import Tool, TextContent - -from ..tool_names import TigerGraphToolName -from ..connection_manager import get_connection - - -class CreateDataSourceToolInput(BaseModel): - """Input schema for creating a data source.""" - data_source_name: str = Field(..., description="Name of the data source.") - data_source_type: str = Field(..., description="Type of data source: 's3', 'gcs', 'azure_blob', or 'local'.") - config: Dict[str, Any] = Field(..., description="Configuration for the data source (e.g., bucket, credentials).") - - -class UpdateDataSourceToolInput(BaseModel): - """Input schema for updating a data source.""" - data_source_name: str = Field(..., description="Name of the data source to update.") - config: Dict[str, Any] = Field(..., description="Updated configuration for the data source.") - - -class GetDataSourceToolInput(BaseModel): - """Input schema for getting a data source.""" - data_source_name: str = Field(..., description="Name of the data source.") - - -class DropDataSourceToolInput(BaseModel): - """Input schema for dropping a data source.""" - data_source_name: str = Field(..., description="Name of the data source to drop.") - - -class GetAllDataSourcesToolInput(BaseModel): - """Input schema for getting all data sources.""" - # No parameters needed - returns all data sources - - -class DropAllDataSourcesToolInput(BaseModel): - """Input schema for dropping all data sources.""" - confirm: bool = Field(False, description="Must be True to confirm dropping all data sources.") - - -class PreviewSampleDataToolInput(BaseModel): - """Input schema for previewing sample data.""" - data_source_name: str = Field(..., description="Name of the data source.") - file_path: str = Field(..., description="Path to the file within the data source.") - num_rows: int = Field(10, description="Number of sample rows to preview.") - graph_name: Optional[str] = Field(None, description="Name of the graph context. If not provided, uses default connection.") - - -create_data_source_tool = Tool( - name=TigerGraphToolName.CREATE_DATA_SOURCE, - description="Create a new data source for loading data (S3, GCS, Azure Blob, or local).", - inputSchema=CreateDataSourceToolInput.model_json_schema(), -) - -update_data_source_tool = Tool( - name=TigerGraphToolName.UPDATE_DATA_SOURCE, - description="Update an existing data source configuration.", - inputSchema=UpdateDataSourceToolInput.model_json_schema(), -) - -get_data_source_tool = Tool( - name=TigerGraphToolName.GET_DATA_SOURCE, - description="Get information about a specific data source.", - inputSchema=GetDataSourceToolInput.model_json_schema(), -) - -drop_data_source_tool = Tool( - name=TigerGraphToolName.DROP_DATA_SOURCE, - description="Drop (delete) a data source.", - inputSchema=DropDataSourceToolInput.model_json_schema(), -) - -get_all_data_sources_tool = Tool( - name=TigerGraphToolName.GET_ALL_DATA_SOURCES, - description="Get information about all data sources.", - inputSchema=GetAllDataSourcesToolInput.model_json_schema(), -) - -drop_all_data_sources_tool = Tool( - name=TigerGraphToolName.DROP_ALL_DATA_SOURCES, - description="Drop all data sources. WARNING: This is a destructive operation.", - inputSchema=DropAllDataSourcesToolInput.model_json_schema(), -) - -preview_sample_data_tool = Tool( - name=TigerGraphToolName.PREVIEW_SAMPLE_DATA, - description="Preview sample data from a file in a data source.", - inputSchema=PreviewSampleDataToolInput.model_json_schema(), -) - - -async def create_data_source( - data_source_name: str, - data_source_type: str, - config: Dict[str, Any], -) -> List[TextContent]: - """Create a new data source.""" - from ..response_formatter import format_success, format_error, gsql_has_error - - try: - conn = get_connection() - - config_str = ", ".join([f'{k}="{v}"' for k, v in config.items()]) - - gsql_cmd = f"CREATE DATA_SOURCE {data_source_type.upper()} {data_source_name}" - if config_str: - gsql_cmd += f" = ({config_str})" - - result = await conn.gsql(gsql_cmd) - result_str = str(result) if result else "" - - if gsql_has_error(result_str): - return format_error( - operation="create_data_source", - error=Exception(f"Could not create data source:\n{result_str}"), - context={"data_source_name": data_source_name, "data_source_type": data_source_type}, - ) - - return format_success( - operation="create_data_source", - summary=f"Data source '{data_source_name}' of type '{data_source_type}' created successfully", - data={"data_source_name": data_source_name, "result": result_str}, - suggestions=[ - f"View data source: get_data_source(data_source_name='{data_source_name}')", - "List all data sources: get_all_data_sources()", - ], - ) - except Exception as e: - return format_error( - operation="create_data_source", - error=e, - context={"data_source_name": data_source_name}, - ) - - -async def update_data_source( - data_source_name: str, - config: Dict[str, Any], -) -> List[TextContent]: - """Update an existing data source.""" - from ..response_formatter import format_success, format_error, gsql_has_error - - try: - conn = get_connection() - - config_str = ", ".join([f'{k}="{v}"' for k, v in config.items()]) - gsql_cmd = f"ALTER DATA_SOURCE {data_source_name} = ({config_str})" - - result = await conn.gsql(gsql_cmd) - result_str = str(result) if result else "" - - if gsql_has_error(result_str): - return format_error( - operation="update_data_source", - error=Exception(f"Could not update data source:\n{result_str}"), - context={"data_source_name": data_source_name}, - ) - - return format_success( - operation="update_data_source", - summary=f"Data source '{data_source_name}' updated successfully", - data={"data_source_name": data_source_name, "result": result_str}, - ) - except Exception as e: - return format_error( - operation="update_data_source", - error=e, - context={"data_source_name": data_source_name}, - ) - - -async def get_data_source( - data_source_name: str, -) -> List[TextContent]: - """Get information about a data source.""" - from ..response_formatter import format_success, format_error, gsql_has_error - - try: - conn = get_connection() - - result = await conn.gsql(f"SHOW DATA_SOURCE {data_source_name}") - result_str = str(result) if result else "" - - if gsql_has_error(result_str): - return format_error( - operation="get_data_source", - error=Exception(f"Could not retrieve data source:\n{result_str}"), - context={"data_source_name": data_source_name}, - ) - - return format_success( - operation="get_data_source", - summary=f"Data source '{data_source_name}' details", - data={"data_source_name": data_source_name, "details": result_str}, - ) - except Exception as e: - return format_error( - operation="get_data_source", - error=e, - context={"data_source_name": data_source_name}, - ) - - -async def drop_data_source( - data_source_name: str, -) -> List[TextContent]: - """Drop a data source.""" - from ..response_formatter import format_success, format_error, gsql_has_error - - try: - conn = get_connection() - - result = await conn.gsql(f"DROP DATA_SOURCE {data_source_name}") - result_str = str(result) if result else "" - - if gsql_has_error(result_str): - return format_error( - operation="drop_data_source", - error=Exception(f"Could not drop data source:\n{result_str}"), - context={"data_source_name": data_source_name}, - ) - - return format_success( - operation="drop_data_source", - summary=f"Data source '{data_source_name}' dropped successfully", - data={"data_source_name": data_source_name, "result": result_str}, - suggestions=["List remaining: get_all_data_sources()"], - metadata={"destructive": True}, - ) - except Exception as e: - return format_error( - operation="drop_data_source", - error=e, - context={"data_source_name": data_source_name}, - ) - - -async def get_all_data_sources(**kwargs) -> List[TextContent]: - """Get all data sources.""" - from ..response_formatter import format_success, format_error, gsql_has_error - - try: - conn = get_connection() - - result = await conn.gsql("SHOW DATA_SOURCE *") - result_str = str(result) if result else "" - - if gsql_has_error(result_str): - return format_error( - operation="get_all_data_sources", - error=Exception(f"Could not retrieve data sources:\n{result_str}"), - context={}, - ) - - return format_success( - operation="get_all_data_sources", - summary="All data sources retrieved", - data={"details": result_str}, - suggestions=["Create a data source: create_data_source(...)"], - ) - except Exception as e: - return format_error( - operation="get_all_data_sources", - error=e, - context={}, - ) - - -async def drop_all_data_sources( - confirm: bool = False, -) -> List[TextContent]: - """Drop all data sources.""" - from ..response_formatter import format_success, format_error, gsql_has_error - - if not confirm: - return format_error( - operation="drop_all_data_sources", - error=ValueError("Confirmation required"), - context={}, - suggestions=[ - "Set confirm=True to proceed with this destructive operation", - "This will drop ALL data sources", - ], - ) - - try: - conn = get_connection() - - result = await conn.gsql("DROP DATA_SOURCE *") - result_str = str(result) if result else "" - - if gsql_has_error(result_str): - return format_error( - operation="drop_all_data_sources", - error=Exception(f"Could not drop all data sources:\n{result_str}"), - context={}, - ) - - return format_success( - operation="drop_all_data_sources", - summary="All data sources dropped successfully", - data={"result": result_str}, - metadata={"destructive": True}, - ) - except Exception as e: - return format_error( - operation="drop_all_data_sources", - error=e, - context={}, - ) - - -async def preview_sample_data( - data_source_name: str, - file_path: str, - num_rows: int = 10, - graph_name: Optional[str] = None, -) -> List[TextContent]: - """Preview sample data from a file.""" - from ..response_formatter import format_success, format_error, gsql_has_error - - try: - conn = get_connection(graph_name=graph_name) - - gsql_cmd = ( - f"USE GRAPH {conn.graphname}\n" - f'SHOW DATA_SOURCE {data_source_name} FILE "{file_path}" LIMIT {num_rows}' - ) - - result = await conn.gsql(gsql_cmd) - result_str = str(result) if result else "" - - if gsql_has_error(result_str): - return format_error( - operation="preview_sample_data", - error=Exception(f"Could not preview data:\n{result_str}"), - context={"data_source_name": data_source_name, "file_path": file_path}, - ) - - return format_success( - operation="preview_sample_data", - summary=f"Sample data from '{file_path}' (first {num_rows} rows)", - data={"data_source_name": data_source_name, "file_path": file_path, "preview": result_str}, - metadata={"graph_name": conn.graphname}, - ) - except Exception as e: - return format_error( - operation="preview_sample_data", - error=e, - context={"data_source_name": data_source_name, "file_path": file_path}, - ) - diff --git a/pyTigerGraph/mcp/tools/discovery_tools.py b/pyTigerGraph/mcp/tools/discovery_tools.py deleted file mode 100644 index a09f6605..00000000 --- a/pyTigerGraph/mcp/tools/discovery_tools.py +++ /dev/null @@ -1,611 +0,0 @@ -# Copyright 2025 TigerGraph Inc. -# Licensed under the Apache License, Version 2.0. -# See the LICENSE file or https://www.apache.org/licenses/LICENSE-2.0 -# -# Permission is granted to use, copy, modify, and distribute this software -# under the License. The software is provided "AS IS", without warranty. - -"""Discovery and navigation tools for LLMs. - -These tools help LLMs discover the right tools for their tasks and understand -common workflows. -""" - -import json -from typing import List, Optional -from pydantic import BaseModel, Field -from mcp.types import Tool, TextContent - -from ..tool_names import TigerGraphToolName -from ..tool_metadata import TOOL_METADATA, ToolCategory, search_tools_by_keywords, get_tools_by_category -from ..response_formatter import format_success, format_list_response - - -class ToolDiscoveryInput(BaseModel): - """Input for discovering relevant tools.""" - task_description: str = Field( - ..., - description=( - "Describe what you want to accomplish in natural language.\n" - "Examples:\n" - " - 'add multiple users to the graph'\n" - " - 'find similar documents using embeddings'\n" - " - 'understand the graph structure'\n" - " - 'load data from a CSV file'" - ) - ) - category: Optional[str] = Field( - None, - description=( - "Filter by category: 'schema', 'data', 'query', 'vector', 'loading', 'utility'.\n" - "Leave empty to search all categories." - ) - ) - limit: int = Field( - 5, - description="Maximum number of tools to return (default: 5)" - ) - - -class GetWorkflowInput(BaseModel): - """Input for getting workflow templates.""" - workflow_type: str = Field( - ..., - description=( - "Type of workflow to retrieve:\n" - " - 'create_graph': Set up a new graph with schema\n" - " - 'load_data': Import data into an existing graph\n" - " - 'query_data': Query and analyze graph data\n" - " - 'vector_search': Set up and use vector similarity search\n" - " - 'graph_analysis': Analyze graph structure and statistics\n" - " - 'setup_connection': Initial connection setup and verification" - ) - ) - - -class GetToolInfoInput(BaseModel): - """Input for getting detailed information about a specific tool.""" - tool_name: str = Field( - ..., - description=( - "Name of the tool to get information about.\n" - "Example: 'tigergraph__add_node' or 'tigergraph__search_top_k_similarity'" - ) - ) - - -# Tool definitions -discover_tools_tool = Tool( - name=TigerGraphToolName.DISCOVER_TOOLS, - description=( - "Discover which TigerGraph tools are relevant for your task.\n\n" - "**Use this tool when:**\n" - " - You're unsure which tool to use for your goal\n" - " - You want to explore available capabilities\n" - " - You need suggestions for accomplishing a task\n\n" - "**Returns:**\n" - " - List of recommended tools with descriptions\n" - " - Use cases and complexity ratings\n" - " - Prerequisites and related tools\n" - " - Example parameters\n\n" - "**Example:**\n" - " task_description: 'I want to add multiple users to the graph'" - ), - inputSchema=ToolDiscoveryInput.model_json_schema(), -) - -get_workflow_tool = Tool( - name=TigerGraphToolName.GET_WORKFLOW, - description=( - "Get a step-by-step workflow template for common TigerGraph tasks.\n\n" - "**Use this tool when:**\n" - " - You need to complete a complex multi-step task\n" - " - You want to follow best practices\n" - " - You're new to TigerGraph and need guidance\n\n" - "**Returns:**\n" - " - Ordered list of tools to use\n" - " - Example parameters for each step\n" - " - Explanations of what each step accomplishes\n\n" - "**Available workflows:** create_graph, load_data, query_data, vector_search, graph_analysis, setup_connection" - ), - inputSchema=GetWorkflowInput.model_json_schema(), -) - -get_tool_info_tool = Tool( - name=TigerGraphToolName.GET_TOOL_INFO, - description=( - "Get detailed information about a specific TigerGraph tool.\n\n" - "**Use this tool when:**\n" - " - You want to understand a tool's capabilities\n" - " - You need examples of how to use a tool\n" - " - You want to know prerequisites or related tools\n\n" - "**Returns:**\n" - " - Detailed tool description\n" - " - Use cases and examples\n" - " - Prerequisites and related tools\n" - " - Common next steps" - ), - inputSchema=GetToolInfoInput.model_json_schema(), -) - - -# Workflow templates -WORKFLOWS = { - "setup_connection": { - "name": "Setup and Verify Connection", - "description": "Initial setup to verify connection and explore available graphs", - "steps": [ - { - "step": 1, - "tool": "tigergraph__list_graphs", - "description": "List all available graphs to see what exists", - "parameters": {}, - "rationale": "First, discover what graphs are available in your TigerGraph instance" - }, - { - "step": 2, - "tool": "tigergraph__show_graph_details", - "description": "Get detailed schema of a specific graph", - "parameters": {"graph_name": "