pggm-mcp-snowflake-server 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pggm_mcp_snowflake_server/__init__.py +110 -2
- pggm_mcp_snowflake_server/server.py +4 -4
- pggm_mcp_snowflake_server/write_detector.py +91 -29
- pggm_mcp_snowflake_server-0.1.1.dist-info/METADATA +67 -0
- pggm_mcp_snowflake_server-0.1.1.dist-info/RECORD +8 -0
- pggm_mcp_snowflake_server-0.1.0.dist-info/METADATA +0 -34
- pggm_mcp_snowflake_server-0.1.0.dist-info/RECORD +0 -8
- {pggm_mcp_snowflake_server-0.1.0.dist-info → pggm_mcp_snowflake_server-0.1.1.dist-info}/WHEEL +0 -0
- {pggm_mcp_snowflake_server-0.1.0.dist-info → pggm_mcp_snowflake_server-0.1.1.dist-info}/entry_points.txt +0 -0
- {pggm_mcp_snowflake_server-0.1.0.dist-info → pggm_mcp_snowflake_server-0.1.1.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,111 @@
|
|
1
|
-
|
1
|
+
import argparse
|
2
|
+
import asyncio
|
3
|
+
import os
|
2
4
|
|
3
|
-
|
5
|
+
import dotenv
|
6
|
+
import snowflake.connector
|
7
|
+
|
8
|
+
from . import server
|
9
|
+
|
10
|
+
|
11
|
+
def parse_args():
|
12
|
+
parser = argparse.ArgumentParser()
|
13
|
+
|
14
|
+
# Add arguments
|
15
|
+
parser.add_argument(
|
16
|
+
"--allow_write", required=False, default=False, action="store_true", help="Allow write operations on the database"
|
17
|
+
)
|
18
|
+
parser.add_argument("--log_dir", required=False, default=None, help="Directory to log to")
|
19
|
+
parser.add_argument("--log_level", required=False, default="INFO", help="Logging level")
|
20
|
+
parser.add_argument(
|
21
|
+
"--prefetch",
|
22
|
+
action="store_true",
|
23
|
+
dest="prefetch",
|
24
|
+
default=True,
|
25
|
+
help="Prefetch table descriptions (when enabled, list_tables and describe_table are disabled)",
|
26
|
+
)
|
27
|
+
parser.add_argument(
|
28
|
+
"--no-prefetch",
|
29
|
+
action="store_false",
|
30
|
+
dest="prefetch",
|
31
|
+
help="Don't prefetch table descriptions",
|
32
|
+
)
|
33
|
+
parser.add_argument(
|
34
|
+
"--exclude_tools",
|
35
|
+
required=False,
|
36
|
+
default=[],
|
37
|
+
nargs="+",
|
38
|
+
help="List of tools to exclude",
|
39
|
+
)
|
40
|
+
|
41
|
+
# First, get all the arguments we don't know about
|
42
|
+
args, unknown = parser.parse_known_args()
|
43
|
+
|
44
|
+
# Create a dictionary to store our key-value pairs
|
45
|
+
connection_args = {}
|
46
|
+
|
47
|
+
# Iterate through unknown args in pairs
|
48
|
+
for i in range(0, len(unknown), 2):
|
49
|
+
if i + 1 >= len(unknown):
|
50
|
+
break
|
51
|
+
|
52
|
+
key = unknown[i]
|
53
|
+
value = unknown[i + 1]
|
54
|
+
|
55
|
+
# Make sure it's a keyword argument (starts with --)
|
56
|
+
if key.startswith("--"):
|
57
|
+
key = key[2:] # Remove the '--'
|
58
|
+
connection_args[key] = value
|
59
|
+
|
60
|
+
# Now we can add the known args to kwargs
|
61
|
+
server_args = {
|
62
|
+
"allow_write": args.allow_write,
|
63
|
+
"log_dir": args.log_dir,
|
64
|
+
"log_level": args.log_level,
|
65
|
+
"prefetch": args.prefetch,
|
66
|
+
"exclude_tools": args.exclude_tools,
|
67
|
+
}
|
68
|
+
|
69
|
+
return server_args, connection_args
|
70
|
+
|
71
|
+
|
72
|
+
def main():
|
73
|
+
"""Main entry point for the package."""
|
74
|
+
|
75
|
+
dotenv.load_dotenv()
|
76
|
+
|
77
|
+
default_connection_args = snowflake.connector.connection.DEFAULT_CONFIGURATION
|
78
|
+
|
79
|
+
connection_args_from_env = {
|
80
|
+
k: os.getenv("SNOWFLAKE_" + k.upper())
|
81
|
+
for k in default_connection_args
|
82
|
+
if os.getenv("SNOWFLAKE_" + k.upper()) is not None
|
83
|
+
}
|
84
|
+
|
85
|
+
server_args, connection_args = parse_args()
|
86
|
+
|
87
|
+
connection_args = {**connection_args_from_env, **connection_args}
|
88
|
+
|
89
|
+
assert (
|
90
|
+
"database" in connection_args
|
91
|
+
), 'You must provide the account identifier as "--database" argument or "SNOWFLAKE_DATABASE" environment variable.'
|
92
|
+
assert (
|
93
|
+
"schema" in connection_args
|
94
|
+
), 'You must provide the username as "--schema" argument or "SNOWFLAKE_SCHEMA" environment variable.'
|
95
|
+
|
96
|
+
asyncio.run(
|
97
|
+
server.main(
|
98
|
+
connection_args=connection_args,
|
99
|
+
allow_write=server_args["allow_write"],
|
100
|
+
log_dir=server_args["log_dir"],
|
101
|
+
log_level=server_args["log_level"],
|
102
|
+
exclude_tools=server_args["exclude_tools"],
|
103
|
+
)
|
104
|
+
)
|
105
|
+
|
106
|
+
|
107
|
+
# Optionally expose other important items at package level
|
108
|
+
__all__ = ["main", "server", "write_detector"]
|
109
|
+
|
110
|
+
if __name__ == "__main__":
|
111
|
+
main()
|
@@ -23,7 +23,7 @@ logging.basicConfig(
|
|
23
23
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
24
24
|
handlers=[logging.StreamHandler()],
|
25
25
|
)
|
26
|
-
logger = logging.getLogger("
|
26
|
+
logger = logging.getLogger("mcp_snowflake_server")
|
27
27
|
|
28
28
|
|
29
29
|
def data_to_yaml(data: Any) -> str:
|
@@ -351,7 +351,7 @@ async def main(
|
|
351
351
|
if log_dir:
|
352
352
|
os.makedirs(log_dir, exist_ok=True)
|
353
353
|
logger.handlers.append(
|
354
|
-
logging.FileHandler(os.path.join(log_dir, "
|
354
|
+
logging.FileHandler(os.path.join(log_dir, "mcp_snowflake_server.log"))
|
355
355
|
)
|
356
356
|
if log_level:
|
357
357
|
logger.setLevel(log_level)
|
@@ -614,10 +614,10 @@ async def main(
|
|
614
614
|
write_stream,
|
615
615
|
InitializationOptions(
|
616
616
|
server_name="snowflake",
|
617
|
-
server_version=importlib.metadata.version("
|
617
|
+
server_version=importlib.metadata.version("mcp_snowflake_server"),
|
618
618
|
capabilities=server.get_capabilities(
|
619
619
|
notification_options=NotificationOptions(),
|
620
620
|
experimental_capabilities={},
|
621
621
|
),
|
622
622
|
),
|
623
|
-
)
|
623
|
+
)
|
@@ -1,36 +1,98 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
import sqlparse
|
2
|
+
from sqlparse.sql import Token, TokenList
|
3
|
+
from sqlparse.tokens import Keyword, DML, DDL
|
4
|
+
from typing import Dict, List, Set, Tuple
|
5
|
+
|
3
6
|
|
7
|
+
class SQLWriteDetector:
|
4
8
|
def __init__(self):
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
# Define sets of keywords that indicate write operations
|
10
|
+
self.dml_write_keywords = {"INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT", "REPLACE"}
|
11
|
+
|
12
|
+
self.ddl_keywords = {"CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"}
|
13
|
+
|
14
|
+
self.dcl_keywords = {"GRANT", "REVOKE"}
|
15
|
+
|
16
|
+
# Combine all write keywords
|
17
|
+
self.write_keywords = self.dml_write_keywords | self.ddl_keywords | self.dcl_keywords
|
18
|
+
|
19
|
+
def analyze_query(self, sql_query: str) -> Dict:
|
11
20
|
"""
|
12
|
-
Analyze
|
13
|
-
|
21
|
+
Analyze a SQL query to determine if it contains write operations.
|
22
|
+
|
14
23
|
Args:
|
15
|
-
|
16
|
-
|
24
|
+
sql_query: The SQL query string to analyze
|
25
|
+
|
17
26
|
Returns:
|
18
|
-
Dictionary
|
27
|
+
Dictionary containing analysis results
|
19
28
|
"""
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
"
|
29
|
+
# Parse the SQL query
|
30
|
+
parsed = sqlparse.parse(sql_query)
|
31
|
+
if not parsed:
|
32
|
+
return {"contains_write": False, "write_operations": set(), "has_cte_write": False}
|
33
|
+
|
34
|
+
# Initialize result tracking
|
35
|
+
found_operations = set()
|
36
|
+
has_cte_write = False
|
37
|
+
|
38
|
+
# Analyze each statement in the query
|
39
|
+
for statement in parsed:
|
40
|
+
# Check for write operations in CTEs (WITH clauses)
|
41
|
+
if self._has_cte(statement):
|
42
|
+
cte_write = self._analyze_cte(statement)
|
43
|
+
if cte_write:
|
44
|
+
has_cte_write = True
|
45
|
+
found_operations.add("CTE_WRITE")
|
46
|
+
|
47
|
+
# Analyze the main query
|
48
|
+
operations = self._find_write_operations(statement)
|
49
|
+
found_operations.update(operations)
|
50
|
+
|
51
|
+
return {
|
52
|
+
"contains_write": bool(found_operations) or has_cte_write,
|
53
|
+
"write_operations": found_operations,
|
54
|
+
"has_cte_write": has_cte_write,
|
24
55
|
}
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
56
|
+
|
57
|
+
def _has_cte(self, statement: TokenList) -> bool:
|
58
|
+
"""Check if the statement has a WITH clause."""
|
59
|
+
return any(token.is_keyword and token.normalized == "WITH" for token in statement.tokens)
|
60
|
+
|
61
|
+
def _analyze_cte(self, statement: TokenList) -> bool:
|
62
|
+
"""
|
63
|
+
Analyze CTEs (WITH clauses) for write operations.
|
64
|
+
Returns True if any CTE contains a write operation.
|
65
|
+
"""
|
66
|
+
in_cte = False
|
67
|
+
for token in statement.tokens:
|
68
|
+
if token.is_keyword and token.normalized == "WITH":
|
69
|
+
in_cte = True
|
70
|
+
elif in_cte:
|
71
|
+
if any(write_kw in token.normalized for write_kw in self.write_keywords):
|
72
|
+
return True
|
73
|
+
return False
|
74
|
+
|
75
|
+
def _find_write_operations(self, statement: TokenList) -> Set[str]:
|
76
|
+
"""
|
77
|
+
Find all write operations in a statement.
|
78
|
+
Returns a set of found write operation keywords.
|
79
|
+
"""
|
80
|
+
operations = set()
|
81
|
+
|
82
|
+
for token in statement.tokens:
|
83
|
+
# Skip comments and whitespace
|
84
|
+
if token.is_whitespace or token.ttype in (sqlparse.tokens.Comment,):
|
85
|
+
continue
|
86
|
+
|
87
|
+
# Check if token is a keyword
|
88
|
+
if token.ttype in (Keyword, DML, DDL):
|
89
|
+
normalized = token.normalized.upper()
|
90
|
+
if normalized in self.write_keywords:
|
91
|
+
operations.add(normalized)
|
92
|
+
|
93
|
+
# Recursively check child tokens
|
94
|
+
if isinstance(token, TokenList):
|
95
|
+
child_ops = self._find_write_operations(token)
|
96
|
+
operations.update(child_ops)
|
97
|
+
|
98
|
+
return operations
|
@@ -0,0 +1,67 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pggm-mcp-snowflake-server
|
3
|
+
Version: 0.1.1
|
4
|
+
Summary: Custom Model Context Protocol server for Snowflake
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
7
|
+
Classifier: Operating System :: OS Independent
|
8
|
+
Requires-Python: >=3.8
|
9
|
+
Description-Content-Type: text/markdown
|
10
|
+
Requires-Dist: mcp
|
11
|
+
Requires-Dist: pydantic
|
12
|
+
Requires-Dist: snowflake-snowpark-python
|
13
|
+
Requires-Dist: pyyaml
|
14
|
+
|
15
|
+
# PGGM MCP Snowflake Server
|
16
|
+
|
17
|
+
A customized Model Context Protocol (MCP) server for Snowflake integration, allowing AI assistants to interact with Snowflake databases securely and efficiently.
|
18
|
+
|
19
|
+
## Features
|
20
|
+
|
21
|
+
- Connect to Snowflake databases and execute queries
|
22
|
+
- Support for various SQL operations and schema exploration
|
23
|
+
- Data insights collection and memoization
|
24
|
+
- SQL write operation detection for enhanced security
|
25
|
+
- Customizable database, schema, and table filtering
|
26
|
+
- Support for authentication through environment variables or command-line arguments
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
### Using pip
|
31
|
+
|
32
|
+
```bash
|
33
|
+
pip install pggm-mcp-snowflake-server
|
34
|
+
```
|
35
|
+
|
36
|
+
|
37
|
+
### Tools Available to AI Assistants
|
38
|
+
|
39
|
+
The server provides the following tools for AI assistants:
|
40
|
+
|
41
|
+
- `list_databases` - List all available databases in Snowflake
|
42
|
+
- `list_schemas` - List all schemas in a database
|
43
|
+
- `list_tables` - List all tables in a specific database and schema
|
44
|
+
- `describe_table` - Get the schema information for a specific table
|
45
|
+
- `read_query` - Execute a SELECT query
|
46
|
+
- `append_insight` - Add a data insight to the memo
|
47
|
+
- `write_query` - Execute an INSERT, UPDATE, or DELETE query (if --allow_write is enabled)
|
48
|
+
- `create_table` - Create a new table in the Snowflake database (if --allow_write is enabled)
|
49
|
+
|
50
|
+
## Security
|
51
|
+
|
52
|
+
By default, the server runs in read-only mode. To enable write operations, you must explicitly pass the `--allow_write` flag.
|
53
|
+
|
54
|
+
The server uses SQL parsing to detect and prevent write operations in `read_query` calls, ensuring only approved write operations can be executed.
|
55
|
+
|
56
|
+
## Development
|
57
|
+
|
58
|
+
### Setup Development Environment
|
59
|
+
|
60
|
+
```bash
|
61
|
+
git clone https://github.com/yourusername/pggm-mcp-snowflake-server.git
|
62
|
+
cd pggm-mcp-snowflake-server
|
63
|
+
uv venv
|
64
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
65
|
+
uv pip install -e ".[dev]"
|
66
|
+
```
|
67
|
+
|
@@ -0,0 +1,8 @@
|
|
1
|
+
pggm_mcp_snowflake_server/__init__.py,sha256=fBi0ejuNSFNA_q0h1Jo6zMlsS0R1_xOW8sne9BygAHQ,3304
|
2
|
+
pggm_mcp_snowflake_server/server.py,sha256=hfINhJuS2s44obtKWvxfLek6Y5P6GmBLUgkhnr3DuD0,21959
|
3
|
+
pggm_mcp_snowflake_server/write_detector.py,sha256=qLFxghERiZT7-DRT8sGPAqrACTgO61QE29QXtH4CN2w,3635
|
4
|
+
pggm_mcp_snowflake_server-0.1.1.dist-info/METADATA,sha256=Okb1U8raBISqfDIvOydkYhL2KJ8cf_Z4Fwc97lHRjHM,2328
|
5
|
+
pggm_mcp_snowflake_server-0.1.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
6
|
+
pggm_mcp_snowflake_server-0.1.1.dist-info/entry_points.txt,sha256=kbUmsaZT0hYi6naFrGUO-mIKlwqHi_HKO6HNQKdTJE4,84
|
7
|
+
pggm_mcp_snowflake_server-0.1.1.dist-info/top_level.txt,sha256=ouamdLwMWx5aSlAHI_mhoPvm9PEBtovD3qbDvR7x284,26
|
8
|
+
pggm_mcp_snowflake_server-0.1.1.dist-info/RECORD,,
|
@@ -1,34 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: pggm-mcp-snowflake-server
|
3
|
-
Version: 0.1.0
|
4
|
-
Summary: Custom Model Context Protocol server for Snowflake
|
5
|
-
Classifier: Programming Language :: Python :: 3
|
6
|
-
Classifier: License :: OSI Approved :: MIT License
|
7
|
-
Classifier: Operating System :: OS Independent
|
8
|
-
Requires-Python: >=3.8
|
9
|
-
Description-Content-Type: text/markdown
|
10
|
-
Requires-Dist: mcp
|
11
|
-
Requires-Dist: pydantic
|
12
|
-
Requires-Dist: snowflake-snowpark-python
|
13
|
-
Requires-Dist: pyyaml
|
14
|
-
|
15
|
-
# PGGM MCP Snowflake Server
|
16
|
-
|
17
|
-
A customized Model Context Protocol (MCP) server for Snowflake integration, allowing AI assistants to interact with Snowflake databases.
|
18
|
-
|
19
|
-
## Features
|
20
|
-
|
21
|
-
- Connect to Snowflake databases and execute queries
|
22
|
-
- Support for various SQL operations and schema exploration
|
23
|
-
- Data insights collection
|
24
|
-
- Customized filters and configurations
|
25
|
-
|
26
|
-
## Installation
|
27
|
-
|
28
|
-
```bash
|
29
|
-
pip install pggm-mcp-snowflake-server
|
30
|
-
```
|
31
|
-
|
32
|
-
## Usage
|
33
|
-
|
34
|
-
This package is designed to be used with MCP-compatible AI assistants for database interactions.
|
@@ -1,8 +0,0 @@
|
|
1
|
-
pggm_mcp_snowflake_server/__init__.py,sha256=bq16COjhclXSrjQP18TV21vWwJV24hIBrf4I7OfwZkE,48
|
2
|
-
pggm_mcp_snowflake_server/server.py,sha256=4_Jlr_o5i4v_39-LuT9CFcqNwGMlNCXSZNgiiSp2vwg,21976
|
3
|
-
pggm_mcp_snowflake_server/write_detector.py,sha256=Zli_U5tnIlCzpVoSYT7jPDBlDUSv4nyS1PnIrXpwZYc,1204
|
4
|
-
pggm_mcp_snowflake_server-0.1.0.dist-info/METADATA,sha256=B6Pgk0AKQdZgmgWK19TZ-Z6jAH1g4YiKkGskqp-skXg,1015
|
5
|
-
pggm_mcp_snowflake_server-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
6
|
-
pggm_mcp_snowflake_server-0.1.0.dist-info/entry_points.txt,sha256=kbUmsaZT0hYi6naFrGUO-mIKlwqHi_HKO6HNQKdTJE4,84
|
7
|
-
pggm_mcp_snowflake_server-0.1.0.dist-info/top_level.txt,sha256=ouamdLwMWx5aSlAHI_mhoPvm9PEBtovD3qbDvR7x284,26
|
8
|
-
pggm_mcp_snowflake_server-0.1.0.dist-info/RECORD,,
|
{pggm_mcp_snowflake_server-0.1.0.dist-info → pggm_mcp_snowflake_server-0.1.1.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|