database-universal-mcp 1.0.0__tar.gz
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.
- database_universal_mcp-1.0.0/.github/workflows/mcp-smithery-publish.yml +40 -0
- database_universal_mcp-1.0.0/.gitignore +7 -0
- database_universal_mcp-1.0.0/.mcp.json +108 -0
- database_universal_mcp-1.0.0/Dockerfile.glama +20 -0
- database_universal_mcp-1.0.0/LICENSE +17 -0
- database_universal_mcp-1.0.0/PKG-INFO +34 -0
- database_universal_mcp-1.0.0/README.md +34 -0
- database_universal_mcp-1.0.0/glama.json +10 -0
- database_universal_mcp-1.0.0/mcp-wrapper.py +85 -0
- database_universal_mcp-1.0.0/package.json +34 -0
- database_universal_mcp-1.0.0/pyproject.toml +27 -0
- database_universal_mcp-1.0.0/pytest.ini +3 -0
- database_universal_mcp-1.0.0/server.py +502 -0
- database_universal_mcp-1.0.0/smithery.yaml +57 -0
- database_universal_mcp-1.0.0/tests/test_server.py +47 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to Smithery
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions: {}
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
name: Publish MCP Server to Smithery
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
attestations: write
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
20
|
+
with:
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
|
|
23
|
+
- name: Setup Node.js
|
|
24
|
+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
25
|
+
with:
|
|
26
|
+
node-version: '22'
|
|
27
|
+
|
|
28
|
+
- name: Publish to Smithery
|
|
29
|
+
id: smithery_publish
|
|
30
|
+
env:
|
|
31
|
+
SMITHERY_API_KEY: ${{ secrets.SMITHERY_API_KEY }}
|
|
32
|
+
run: |
|
|
33
|
+
npx @smithery/cli mcp publish "https://github.com/${{ github.repository }}" -n nicholastempleman/${{ github.event.repository.name }} --json
|
|
34
|
+
|
|
35
|
+
- name: Attest build provenance
|
|
36
|
+
uses: actions/attest-build-provenance@v2
|
|
37
|
+
with:
|
|
38
|
+
subject-name: ${{ github.repository }}
|
|
39
|
+
subject-digest: sha256:${{ github.sha }}
|
|
40
|
+
push-to-registry: false
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "database-universal-mcp",
|
|
3
|
+
"description": "MCP server for database universal. Features query sql, list tables, describe table. From MEOK AI Labs.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"tools": [
|
|
6
|
+
{
|
|
7
|
+
"name": "query_sql",
|
|
8
|
+
"description": "Execute a SQL query against a database. Supports SQLite, PostgreSQL, and MySQL.",
|
|
9
|
+
"parameters": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"connection_string": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"sql": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"allow_write": {
|
|
19
|
+
"type": "boolean"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": [
|
|
23
|
+
"connection_string",
|
|
24
|
+
"sql"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "list_tables",
|
|
30
|
+
"description": "List all tables in a database.",
|
|
31
|
+
"parameters": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"connection_string": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": [
|
|
39
|
+
"connection_string"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "describe_table",
|
|
45
|
+
"description": "Describe a table's schema: column names, types, nullability, defaults,",
|
|
46
|
+
"parameters": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"connection_string": {
|
|
50
|
+
"type": "string"
|
|
51
|
+
},
|
|
52
|
+
"table_name": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"required": [
|
|
57
|
+
"connection_string",
|
|
58
|
+
"table_name"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "insert_row",
|
|
64
|
+
"description": "Insert a single row into a table. Column names and values are passed",
|
|
65
|
+
"parameters": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"connection_string": {
|
|
69
|
+
"type": "string"
|
|
70
|
+
},
|
|
71
|
+
"table_name": {
|
|
72
|
+
"type": "string"
|
|
73
|
+
},
|
|
74
|
+
"data": {
|
|
75
|
+
"type": "object"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"required": [
|
|
79
|
+
"connection_string",
|
|
80
|
+
"table_name",
|
|
81
|
+
"data"
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "export_to_csv",
|
|
87
|
+
"description": "Execute a SELECT query and export results to a CSV file.",
|
|
88
|
+
"parameters": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {
|
|
91
|
+
"connection_string": {
|
|
92
|
+
"type": "string"
|
|
93
|
+
},
|
|
94
|
+
"sql": {
|
|
95
|
+
"type": "string"
|
|
96
|
+
},
|
|
97
|
+
"output_path": {
|
|
98
|
+
"type": "string"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"required": [
|
|
102
|
+
"connection_string",
|
|
103
|
+
"sql"
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
FROM python:3.14-slim
|
|
2
|
+
|
|
3
|
+
ENV PYTHONUNBUFFERED=1
|
|
4
|
+
ENV PYTHONDONTWRITEBYTECODE=1
|
|
5
|
+
|
|
6
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git build-essential && rm -rf /var/lib/apt/lists/*
|
|
7
|
+
RUN pip install --no-cache-dir uv
|
|
8
|
+
|
|
9
|
+
RUN useradd -m -s /bin/bash nicholas && mkdir -p /home/nicholas/clawd/meok-labs-engine/shared && chown -R nicholas:nicholas /home/nicholas
|
|
10
|
+
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
USER nicholas
|
|
13
|
+
|
|
14
|
+
RUN uv venv /home/nicholas/.venv
|
|
15
|
+
ENV PATH="/home/nicholas/.venv/bin:$PATH"
|
|
16
|
+
|
|
17
|
+
COPY --chown=nicholas:nicholas . /app
|
|
18
|
+
RUN uv pip install -e .
|
|
19
|
+
|
|
20
|
+
CMD ["python", "mcp-wrapper.py"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MEOK AI Labs (meok.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: database-universal-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server for database universal. Features query sql, list tables, describe table. From MEOK AI Labs.
|
|
5
|
+
Project-URL: Homepage, https://meok.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/CSOAI-ORG/database-universal-mcp
|
|
7
|
+
Author-email: MEOK AI Labs <nicholas@meok.ai>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 MEOK AI Labs (meok.ai)
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Keywords: ai,database,mcp,mcp/,meok,universal
|
|
27
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
28
|
+
Classifier: Operating System :: OS Independent
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Requires-Dist: mcp>=1.0.0
|
|
33
|
+
Requires-Dist: psycopg2-binary>=2.9.0
|
|
34
|
+
Requires-Dist: pymysql>=1.0.0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Database Universal MCP Server
|
|
2
|
+
|
|
3
|
+
> By [MEOK AI Labs](https://meok.ai) — Connect to SQLite, PostgreSQL, or MySQL databases for querying, schema exploration, and data export
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install database-universal-mcp
|
|
9
|
+
# Optional: pip install psycopg2-binary mysql-connector-python
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python server.py
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Tools
|
|
19
|
+
|
|
20
|
+
This server provides universal database access including:
|
|
21
|
+
|
|
22
|
+
- Connect to SQLite, PostgreSQL, or MySQL databases
|
|
23
|
+
- Execute SQL queries with safety guards
|
|
24
|
+
- Explore database schema and table structure
|
|
25
|
+
- Insert and update data
|
|
26
|
+
- Export query results to CSV format
|
|
27
|
+
|
|
28
|
+
## Authentication
|
|
29
|
+
|
|
30
|
+
Free tier: 30 calls/day. Upgrade at [meok.ai/pricing](https://meok.ai/pricing) for unlimited access.
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT — MEOK AI Labs
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "database-universal-mcp",
|
|
3
|
+
"description": "MEOK AI Labs \u2014 database-universal-mcp",
|
|
4
|
+
"vendor": "MEOK AI Labs",
|
|
5
|
+
"homepage": "https://meok.ai",
|
|
6
|
+
"repository": "https://github.com/CSOAI-ORG/database-universal-mcp",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"runtime": "python",
|
|
9
|
+
"entryPoint": "mcp-wrapper.py"
|
|
10
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""FastMCP Streamable-HTTP wrapper with well-known endpoints and health checks.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python /path/to/mcp-streamable-http-wrapper.py
|
|
6
|
+
|
|
7
|
+
This imports `mcp` from `server.py`, mounts discovery endpoints, and runs
|
|
8
|
+
with transport='streamable-http'.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.expanduser("~/clawd/meok-labs-engine/shared"))
|
|
16
|
+
sys.path.insert(0, os.getcwd())
|
|
17
|
+
|
|
18
|
+
from starlette.requests import Request
|
|
19
|
+
from starlette.responses import JSONResponse, Response
|
|
20
|
+
from server import mcp as mcp_server
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SERVICE_NAME = os.path.basename(os.getcwd())
|
|
24
|
+
REPO_URL = f"https://github.com/CSOAI-ORG/{SERVICE_NAME}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@mcp_server.custom_route("/.well-known/mcp/server-card.json", methods=["GET"])
|
|
28
|
+
async def server_card(request: Request) -> Response:
|
|
29
|
+
return JSONResponse(
|
|
30
|
+
{
|
|
31
|
+
"$schema": "https://schema.smithery.ai/server-card.json",
|
|
32
|
+
"version": "1.0.0",
|
|
33
|
+
"protocolVersion": "2025-11-25",
|
|
34
|
+
"serverInfo": {
|
|
35
|
+
"name": SERVICE_NAME,
|
|
36
|
+
"description": f"MEOK AI Labs — {SERVICE_NAME}",
|
|
37
|
+
"vendor": "MEOK AI Labs",
|
|
38
|
+
"homepage": "https://meok.ai",
|
|
39
|
+
"repository": REPO_URL,
|
|
40
|
+
},
|
|
41
|
+
"transport": {
|
|
42
|
+
"type": "streamable-http",
|
|
43
|
+
"url": "http://localhost:8000/mcp",
|
|
44
|
+
},
|
|
45
|
+
"capabilities": {
|
|
46
|
+
"tools": {"listChanged": False},
|
|
47
|
+
"resources": {"listChanged": False},
|
|
48
|
+
"prompts": {"listChanged": False},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
headers={
|
|
52
|
+
"Access-Control-Allow-Origin": "*",
|
|
53
|
+
"Cache-Control": "public, max-age=3600",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp_server.custom_route("/.well-known/mcp", methods=["GET"])
|
|
59
|
+
async def mcp_manifest(request: Request) -> Response:
|
|
60
|
+
return JSONResponse(
|
|
61
|
+
{
|
|
62
|
+
"mcp_version": "2025-11-25",
|
|
63
|
+
"endpoints": [
|
|
64
|
+
{
|
|
65
|
+
"type": "streamable-http",
|
|
66
|
+
"path": "/mcp",
|
|
67
|
+
"url": "http://localhost:8000/mcp",
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
headers={
|
|
72
|
+
"Access-Control-Allow-Origin": "*",
|
|
73
|
+
"Cache-Control": "public, max-age=3600",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@mcp_server.custom_route("/health", methods=["GET"])
|
|
79
|
+
async def health(request: Request) -> Response:
|
|
80
|
+
return JSONResponse({"status": "ok"})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
mcp_server.settings.host = "0.0.0.0"
|
|
85
|
+
mcp_server.run(transport="streamable-http")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "database-universal-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for database universal. Features query sql, list tables, describe table. From MEOK AI Labs.",
|
|
5
|
+
"main": "server.py",
|
|
6
|
+
"mcp": {
|
|
7
|
+
"name": "database universal",
|
|
8
|
+
"vendor": "MEOK AI Labs",
|
|
9
|
+
"homepage": "https://meok.ai",
|
|
10
|
+
"repository": "https://github.com/CSOAI-ORG/database-universal-mcp",
|
|
11
|
+
"runtime": "python",
|
|
12
|
+
"tags": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"mcp-server",
|
|
15
|
+
"meok-ai-labs",
|
|
16
|
+
"ai-tools"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"mcp-server",
|
|
22
|
+
"meok-ai-labs"
|
|
23
|
+
],
|
|
24
|
+
"author": "MEOK AI Labs <nicholas@meok.ai>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/CSOAI-ORG/database-universal-mcp"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"psycopg2-binary": "^1.0.0",
|
|
32
|
+
"PyMySQL": "^1.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
[project]
|
|
5
|
+
name = "database-universal-mcp"
|
|
6
|
+
version = "1.0.0"
|
|
7
|
+
description = "MCP server for database universal. Features query sql, list tables, describe table. From MEOK AI Labs."
|
|
8
|
+
license = {file = "LICENSE"}
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
authors = [{name = "MEOK AI Labs", email = "nicholas@meok.ai"}]
|
|
11
|
+
dependencies = ["mcp>=1.0.0", "psycopg2-binary>=2.9.0", "PyMySQL>=1.0.0", ]
|
|
12
|
+
keywords = ["mcp", "ai", "meok", "database", "universal", "mcp/"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Software Development :: Libraries",
|
|
18
|
+
]
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://meok.ai"
|
|
21
|
+
Repository = "https://github.com/CSOAI-ORG/database-universal-mcp"
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["."]
|
|
24
|
+
only-include = ["server.py"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
database_universal_mcp = "server:main"
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Universal Database MCP Server
|
|
4
|
+
===============================
|
|
5
|
+
Connect to SQLite, PostgreSQL, or MySQL databases from AI agents.
|
|
6
|
+
Query, explore schema, insert data, and export results to CSV.
|
|
7
|
+
|
|
8
|
+
By MEOK AI Labs | https://meok.ai
|
|
9
|
+
|
|
10
|
+
Install: pip install mcp
|
|
11
|
+
Optional: pip install psycopg2-binary mysql-connector-python
|
|
12
|
+
Run: python server.py
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import sys, os
|
|
17
|
+
sys.path.insert(0, os.path.expanduser('~/clawd/meok-labs-engine/shared'))
|
|
18
|
+
from auth_middleware import check_access
|
|
19
|
+
|
|
20
|
+
import csv
|
|
21
|
+
import io
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import sqlite3
|
|
26
|
+
import tempfile
|
|
27
|
+
from datetime import datetime, timedelta
|
|
28
|
+
from typing import Any, Optional
|
|
29
|
+
from collections import defaultdict
|
|
30
|
+
from urllib.parse import urlparse
|
|
31
|
+
from mcp.server.fastmcp import FastMCP
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Rate limiting
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
FREE_DAILY_LIMIT = 30
|
|
37
|
+
_usage: dict[str, list[datetime]] = defaultdict(list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _check_rate_limit(caller: str = "anonymous") -> Optional[str]:
|
|
41
|
+
now = datetime.now()
|
|
42
|
+
cutoff = now - timedelta(days=1)
|
|
43
|
+
_usage[caller] = [t for t in _usage[caller] if t > cutoff]
|
|
44
|
+
if len(_usage[caller]) >= FREE_DAILY_LIMIT:
|
|
45
|
+
return f"Free tier limit reached ({FREE_DAILY_LIMIT}/day). Upgrade to Pro: https://mcpize.com/database-universal-mcp/pro"
|
|
46
|
+
_usage[caller].append(now)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Safety: SQL query validation
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
_DANGEROUS_PATTERNS = [
|
|
54
|
+
r"\bDROP\s+(TABLE|DATABASE|INDEX|SCHEMA)\b",
|
|
55
|
+
r"\bTRUNCATE\b",
|
|
56
|
+
r"\bALTER\s+TABLE\b.*\bDROP\b",
|
|
57
|
+
r"\bDELETE\s+FROM\b(?!.*\bWHERE\b)", # DELETE without WHERE
|
|
58
|
+
r"\bGRANT\b",
|
|
59
|
+
r"\bREVOKE\b",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _validate_query(sql: str, allow_write: bool = False) -> Optional[str]:
|
|
64
|
+
"""Validate SQL query for safety. Returns error message if unsafe."""
|
|
65
|
+
sql_upper = sql.strip().upper()
|
|
66
|
+
|
|
67
|
+
for pattern in _DANGEROUS_PATTERNS:
|
|
68
|
+
if re.search(pattern, sql_upper, re.IGNORECASE):
|
|
69
|
+
return f"Blocked: dangerous SQL pattern detected. Use with caution or upgrade to Pro for unrestricted access."
|
|
70
|
+
|
|
71
|
+
if not allow_write:
|
|
72
|
+
write_keywords = ["INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP"]
|
|
73
|
+
first_word = sql_upper.split()[0] if sql_upper.split() else ""
|
|
74
|
+
if first_word in write_keywords:
|
|
75
|
+
return f"Write operations require explicit allow_write=True for safety."
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Database connection helpers
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def _get_connection(connection_string: str):
|
|
85
|
+
"""Create a database connection from a connection string.
|
|
86
|
+
|
|
87
|
+
Supported formats:
|
|
88
|
+
- sqlite:///path/to/db.sqlite
|
|
89
|
+
- sqlite:path/to/db.sqlite
|
|
90
|
+
- postgresql://user:pass@host:port/dbname
|
|
91
|
+
- mysql://user:pass@host:port/dbname
|
|
92
|
+
- /path/to/file.db (treated as SQLite)
|
|
93
|
+
"""
|
|
94
|
+
cs = connection_string.strip()
|
|
95
|
+
|
|
96
|
+
# Plain file path -> SQLite
|
|
97
|
+
if cs.startswith("/") or cs.startswith("./") or cs.endswith(".db") or cs.endswith(".sqlite"):
|
|
98
|
+
return sqlite3.connect(cs), "sqlite"
|
|
99
|
+
|
|
100
|
+
parsed = urlparse(cs)
|
|
101
|
+
scheme = parsed.scheme.lower()
|
|
102
|
+
|
|
103
|
+
if scheme in ("sqlite", "sqlite3"):
|
|
104
|
+
path = parsed.path
|
|
105
|
+
if path.startswith("///"):
|
|
106
|
+
path = path[2:] # sqlite:///absolute/path
|
|
107
|
+
elif path.startswith("/"):
|
|
108
|
+
path = path # sqlite:/absolute/path
|
|
109
|
+
return sqlite3.connect(path), "sqlite"
|
|
110
|
+
|
|
111
|
+
elif scheme in ("postgresql", "postgres", "psycopg2"):
|
|
112
|
+
try:
|
|
113
|
+
import psycopg2
|
|
114
|
+
except ImportError:
|
|
115
|
+
raise ImportError("Install psycopg2-binary: pip install psycopg2-binary")
|
|
116
|
+
conn = psycopg2.connect(
|
|
117
|
+
host=parsed.hostname or "localhost",
|
|
118
|
+
port=parsed.port or 5432,
|
|
119
|
+
user=parsed.username or "postgres",
|
|
120
|
+
password=parsed.password or "",
|
|
121
|
+
dbname=parsed.path.lstrip("/") or "postgres")
|
|
122
|
+
conn.autocommit = True
|
|
123
|
+
return conn, "postgresql"
|
|
124
|
+
|
|
125
|
+
elif scheme in ("mysql", "mysql+pymysql"):
|
|
126
|
+
try:
|
|
127
|
+
import mysql.connector
|
|
128
|
+
except ImportError:
|
|
129
|
+
raise ImportError("Install mysql-connector-python: pip install mysql-connector-python")
|
|
130
|
+
conn = mysql.connector.connect(
|
|
131
|
+
host=parsed.hostname or "localhost",
|
|
132
|
+
port=parsed.port or 3306,
|
|
133
|
+
user=parsed.username or "root",
|
|
134
|
+
password=parsed.password or "",
|
|
135
|
+
database=parsed.path.lstrip("/") or "")
|
|
136
|
+
conn.autocommit = True
|
|
137
|
+
return conn, "mysql"
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError(f"Unsupported database scheme: {scheme}. Use sqlite, postgresql, or mysql.")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _execute_query(connection_string: str, sql: str, params: Optional[list] = None) -> dict:
|
|
144
|
+
"""Execute a SQL query and return results."""
|
|
145
|
+
conn, db_type = _get_connection(connection_string)
|
|
146
|
+
try:
|
|
147
|
+
cursor = conn.cursor()
|
|
148
|
+
if params:
|
|
149
|
+
cursor.execute(sql, params)
|
|
150
|
+
else:
|
|
151
|
+
cursor.execute(sql)
|
|
152
|
+
|
|
153
|
+
# Check if query returns rows
|
|
154
|
+
if cursor.description:
|
|
155
|
+
columns = [desc[0] for desc in cursor.description]
|
|
156
|
+
rows = cursor.fetchmany(1000) # Limit to 1000 rows in free tier
|
|
157
|
+
total_available = len(rows)
|
|
158
|
+
if len(rows) == 1000:
|
|
159
|
+
# Try to get count
|
|
160
|
+
try:
|
|
161
|
+
remaining = cursor.fetchall()
|
|
162
|
+
total_available += len(remaining)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
# Convert to list of dicts
|
|
167
|
+
data = []
|
|
168
|
+
for row in rows[:1000]:
|
|
169
|
+
record = {}
|
|
170
|
+
for i, col in enumerate(columns):
|
|
171
|
+
val = row[i]
|
|
172
|
+
# Make JSON-serializable
|
|
173
|
+
if isinstance(val, (datetime)):
|
|
174
|
+
val = val.isoformat()
|
|
175
|
+
elif isinstance(val, bytes):
|
|
176
|
+
val = val.hex()[:100] + "..." if len(val) > 50 else val.hex()
|
|
177
|
+
elif isinstance(val, memoryview):
|
|
178
|
+
val = bytes(val).hex()[:100]
|
|
179
|
+
record[col] = val
|
|
180
|
+
data.append(record)
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"status": "ok",
|
|
184
|
+
"columns": columns,
|
|
185
|
+
"rows": data,
|
|
186
|
+
"row_count": len(data),
|
|
187
|
+
"total_available": total_available,
|
|
188
|
+
"db_type": db_type,
|
|
189
|
+
}
|
|
190
|
+
else:
|
|
191
|
+
affected = cursor.rowcount if cursor.rowcount >= 0 else 0
|
|
192
|
+
conn.commit()
|
|
193
|
+
return {
|
|
194
|
+
"status": "ok",
|
|
195
|
+
"message": f"Query executed successfully. {affected} row(s) affected.",
|
|
196
|
+
"rows_affected": affected,
|
|
197
|
+
"db_type": db_type,
|
|
198
|
+
}
|
|
199
|
+
finally:
|
|
200
|
+
conn.close()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _list_tables(connection_string: str) -> dict:
|
|
204
|
+
"""List all tables in the database."""
|
|
205
|
+
conn, db_type = _get_connection(connection_string)
|
|
206
|
+
try:
|
|
207
|
+
cursor = conn.cursor()
|
|
208
|
+
if db_type == "sqlite":
|
|
209
|
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
210
|
+
elif db_type == "postgresql":
|
|
211
|
+
cursor.execute("""
|
|
212
|
+
SELECT table_name FROM information_schema.tables
|
|
213
|
+
WHERE table_schema = 'public' ORDER BY table_name
|
|
214
|
+
""")
|
|
215
|
+
elif db_type == "mysql":
|
|
216
|
+
cursor.execute("SHOW TABLES")
|
|
217
|
+
|
|
218
|
+
tables = [row[0] for row in cursor.fetchall()]
|
|
219
|
+
return {"status": "ok", "tables": tables, "count": len(tables), "db_type": db_type}
|
|
220
|
+
finally:
|
|
221
|
+
conn.close()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _describe_table(connection_string: str, table_name: str) -> dict:
|
|
225
|
+
"""Describe a table's schema."""
|
|
226
|
+
# Validate table name (prevent injection)
|
|
227
|
+
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', table_name):
|
|
228
|
+
return {"error": "Invalid table name"}
|
|
229
|
+
|
|
230
|
+
conn, db_type = _get_connection(connection_string)
|
|
231
|
+
try:
|
|
232
|
+
cursor = conn.cursor()
|
|
233
|
+
columns = []
|
|
234
|
+
|
|
235
|
+
if db_type == "sqlite":
|
|
236
|
+
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
237
|
+
for row in cursor.fetchall():
|
|
238
|
+
columns.append({
|
|
239
|
+
"name": row[1],
|
|
240
|
+
"type": row[2],
|
|
241
|
+
"nullable": not row[3],
|
|
242
|
+
"default": row[4],
|
|
243
|
+
"primary_key": bool(row[5]),
|
|
244
|
+
})
|
|
245
|
+
# Row count
|
|
246
|
+
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
247
|
+
row_count = cursor.fetchone()[0]
|
|
248
|
+
|
|
249
|
+
elif db_type == "postgresql":
|
|
250
|
+
cursor.execute("""
|
|
251
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
252
|
+
FROM information_schema.columns
|
|
253
|
+
WHERE table_name = %s AND table_schema = 'public'
|
|
254
|
+
ORDER BY ordinal_position
|
|
255
|
+
""", (table_name))
|
|
256
|
+
for row in cursor.fetchall():
|
|
257
|
+
columns.append({
|
|
258
|
+
"name": row[0],
|
|
259
|
+
"type": row[1],
|
|
260
|
+
"nullable": row[2] == "YES",
|
|
261
|
+
"default": row[3],
|
|
262
|
+
})
|
|
263
|
+
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
264
|
+
row_count = cursor.fetchone()[0]
|
|
265
|
+
|
|
266
|
+
elif db_type == "mysql":
|
|
267
|
+
cursor.execute(f"DESCRIBE {table_name}")
|
|
268
|
+
for row in cursor.fetchall():
|
|
269
|
+
columns.append({
|
|
270
|
+
"name": row[0],
|
|
271
|
+
"type": row[1],
|
|
272
|
+
"nullable": row[2] == "YES",
|
|
273
|
+
"default": row[4],
|
|
274
|
+
"primary_key": row[3] == "PRI",
|
|
275
|
+
})
|
|
276
|
+
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
277
|
+
row_count = cursor.fetchone()[0]
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
"status": "ok",
|
|
281
|
+
"table": table_name,
|
|
282
|
+
"columns": columns,
|
|
283
|
+
"column_count": len(columns),
|
|
284
|
+
"row_count": row_count,
|
|
285
|
+
"db_type": db_type,
|
|
286
|
+
}
|
|
287
|
+
finally:
|
|
288
|
+
conn.close()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _insert_row(connection_string: str, table_name: str, data: dict) -> dict:
|
|
292
|
+
"""Insert a row into a table."""
|
|
293
|
+
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', table_name):
|
|
294
|
+
return {"error": "Invalid table name"}
|
|
295
|
+
|
|
296
|
+
conn, db_type = _get_connection(connection_string)
|
|
297
|
+
try:
|
|
298
|
+
cursor = conn.cursor()
|
|
299
|
+
columns = list(data.keys())
|
|
300
|
+
values = list(data.values())
|
|
301
|
+
|
|
302
|
+
# Validate column names
|
|
303
|
+
for col in columns:
|
|
304
|
+
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', col):
|
|
305
|
+
return {"error": f"Invalid column name: {col}"}
|
|
306
|
+
|
|
307
|
+
col_str = ", ".join(columns)
|
|
308
|
+
|
|
309
|
+
if db_type == "sqlite":
|
|
310
|
+
placeholders = ", ".join(["?"] * len(values))
|
|
311
|
+
else:
|
|
312
|
+
placeholders = ", ".join(["%s"] * len(values))
|
|
313
|
+
|
|
314
|
+
sql = f"INSERT INTO {table_name} ({col_str}) VALUES ({placeholders})"
|
|
315
|
+
cursor.execute(sql, values)
|
|
316
|
+
conn.commit()
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
"status": "ok",
|
|
320
|
+
"message": f"Inserted 1 row into {table_name}",
|
|
321
|
+
"table": table_name,
|
|
322
|
+
"columns": columns,
|
|
323
|
+
}
|
|
324
|
+
finally:
|
|
325
|
+
conn.close()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _validate_output_path(output_path: str) -> Optional[str]:
|
|
329
|
+
"""Validate output file path against traversal attacks."""
|
|
330
|
+
blocked = ["/etc/", "/var/", "/proc/", "/sys/", "/dev/", ".."]
|
|
331
|
+
for pattern in blocked:
|
|
332
|
+
if pattern in output_path:
|
|
333
|
+
return f"Access denied: path contains blocked pattern '{pattern}'"
|
|
334
|
+
real = os.path.realpath(output_path)
|
|
335
|
+
parent = os.path.dirname(real)
|
|
336
|
+
if not os.path.isdir(parent):
|
|
337
|
+
return f"Directory does not exist: {parent}"
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _export_to_csv(connection_string: str, sql: str, output_path: str) -> dict:
|
|
342
|
+
"""Execute a query and export results to CSV."""
|
|
343
|
+
path_err = _validate_output_path(output_path)
|
|
344
|
+
if path_err:
|
|
345
|
+
return {"error": path_err}
|
|
346
|
+
|
|
347
|
+
result = _execute_query(connection_string, sql)
|
|
348
|
+
if result.get("status") != "ok":
|
|
349
|
+
return result
|
|
350
|
+
|
|
351
|
+
rows = result.get("rows", [])
|
|
352
|
+
columns = result.get("columns", [])
|
|
353
|
+
|
|
354
|
+
if not rows:
|
|
355
|
+
return {"error": "No data to export"}
|
|
356
|
+
|
|
357
|
+
with open(output_path, "w", newline="") as f:
|
|
358
|
+
writer = csv.DictWriter(f, fieldnames=columns)
|
|
359
|
+
writer.writeheader()
|
|
360
|
+
writer.writerows(rows)
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
"status": "ok",
|
|
364
|
+
"output": output_path,
|
|
365
|
+
"rows_exported": len(rows),
|
|
366
|
+
"columns": columns,
|
|
367
|
+
"file_size_bytes": os.path.getsize(output_path),
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# MCP Server
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
mcp = FastMCP(
|
|
375
|
+
"Universal Database MCP",
|
|
376
|
+
instructions="Database connector for SQLite, PostgreSQL, and MySQL. Query data, explore schema, insert rows, and export to CSV. By MEOK AI Labs.")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@mcp.tool()
|
|
380
|
+
def query_sql(connection_string: str, sql: str, allow_write: bool = False, api_key: str = "") -> dict:
|
|
381
|
+
"""Execute a SQL query against a database. Supports SQLite, PostgreSQL, and MySQL.
|
|
382
|
+
|
|
383
|
+
Connection string examples:
|
|
384
|
+
- SQLite: 'sqlite:///path/to/db.sqlite' or just '/path/to/file.db'
|
|
385
|
+
- PostgreSQL: 'postgresql://user:pass@localhost:5432/mydb'
|
|
386
|
+
- MySQL: 'mysql://user:pass@localhost:3306/mydb'
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
connection_string: Database connection URI
|
|
390
|
+
sql: SQL query to execute
|
|
391
|
+
allow_write: Set True for INSERT/UPDATE/DELETE (safety guard)
|
|
392
|
+
"""
|
|
393
|
+
allowed, msg, tier = check_access(api_key)
|
|
394
|
+
if not allowed:
|
|
395
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
396
|
+
|
|
397
|
+
err = _check_rate_limit()
|
|
398
|
+
if err:
|
|
399
|
+
return {"error": err}
|
|
400
|
+
safety = _validate_query(sql, allow_write)
|
|
401
|
+
if safety:
|
|
402
|
+
return {"error": safety}
|
|
403
|
+
try:
|
|
404
|
+
return _execute_query(connection_string, sql)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
return {"error": str(e)}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@mcp.tool()
|
|
410
|
+
def list_tables(connection_string: str, api_key: str = "") -> dict:
|
|
411
|
+
"""List all tables in a database.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
connection_string: Database connection URI
|
|
415
|
+
"""
|
|
416
|
+
allowed, msg, tier = check_access(api_key)
|
|
417
|
+
if not allowed:
|
|
418
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
419
|
+
|
|
420
|
+
err = _check_rate_limit()
|
|
421
|
+
if err:
|
|
422
|
+
return {"error": err}
|
|
423
|
+
try:
|
|
424
|
+
return _list_tables(connection_string)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
return {"error": str(e)}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@mcp.tool()
|
|
430
|
+
def describe_table(connection_string: str, table_name: str, api_key: str = "") -> dict:
|
|
431
|
+
"""Describe a table's schema: column names, types, nullability, defaults,
|
|
432
|
+
primary keys, and row count.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
connection_string: Database connection URI
|
|
436
|
+
table_name: Name of the table to describe
|
|
437
|
+
"""
|
|
438
|
+
allowed, msg, tier = check_access(api_key)
|
|
439
|
+
if not allowed:
|
|
440
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
441
|
+
|
|
442
|
+
err = _check_rate_limit()
|
|
443
|
+
if err:
|
|
444
|
+
return {"error": err}
|
|
445
|
+
try:
|
|
446
|
+
return _describe_table(connection_string, table_name)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
return {"error": str(e)}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@mcp.tool()
|
|
452
|
+
def insert_row(connection_string: str, table_name: str, data: dict, api_key: str = "") -> dict:
|
|
453
|
+
"""Insert a single row into a table. Column names and values are passed
|
|
454
|
+
as a dictionary.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
connection_string: Database connection URI
|
|
458
|
+
table_name: Target table name
|
|
459
|
+
data: Dictionary of column_name -> value pairs
|
|
460
|
+
"""
|
|
461
|
+
allowed, msg, tier = check_access(api_key)
|
|
462
|
+
if not allowed:
|
|
463
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
464
|
+
|
|
465
|
+
err = _check_rate_limit()
|
|
466
|
+
if err:
|
|
467
|
+
return {"error": err}
|
|
468
|
+
try:
|
|
469
|
+
return _insert_row(connection_string, table_name, data)
|
|
470
|
+
except Exception as e:
|
|
471
|
+
return {"error": str(e)}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@mcp.tool()
|
|
475
|
+
def export_to_csv(connection_string: str, sql: str, output_path: str = "", api_key: str = "") -> dict:
|
|
476
|
+
"""Execute a SELECT query and export results to a CSV file.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
connection_string: Database connection URI
|
|
480
|
+
sql: SELECT query to execute
|
|
481
|
+
output_path: Path for the output CSV (default: temp file)
|
|
482
|
+
"""
|
|
483
|
+
allowed, msg, tier = check_access(api_key)
|
|
484
|
+
if not allowed:
|
|
485
|
+
return {"error": msg, "upgrade_url": "https://meok.ai/pricing"}
|
|
486
|
+
|
|
487
|
+
err = _check_rate_limit()
|
|
488
|
+
if err:
|
|
489
|
+
return {"error": err}
|
|
490
|
+
safety = _validate_query(sql, allow_write=False)
|
|
491
|
+
if safety:
|
|
492
|
+
return {"error": safety}
|
|
493
|
+
if not output_path:
|
|
494
|
+
output_path = os.path.join(tempfile.gettempdir(), f"export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv")
|
|
495
|
+
try:
|
|
496
|
+
return _export_to_csv(connection_string, sql, output_path)
|
|
497
|
+
except Exception as e:
|
|
498
|
+
return {"error": str(e)}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
mcp.run()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: database-universal-mcp
|
|
2
|
+
description: MCP server for database universal. Features query sql, list tables, describe
|
|
3
|
+
table. From MEOK AI Labs.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tools:
|
|
6
|
+
- name: query_sql
|
|
7
|
+
description: Execute a SQL query against a database. Supports SQLite, PostgreSQL,
|
|
8
|
+
and MySQL.
|
|
9
|
+
parameters:
|
|
10
|
+
- name: connection_string
|
|
11
|
+
type: string
|
|
12
|
+
required: true
|
|
13
|
+
- name: sql
|
|
14
|
+
type: string
|
|
15
|
+
required: true
|
|
16
|
+
- name: allow_write
|
|
17
|
+
type: boolean
|
|
18
|
+
required: false
|
|
19
|
+
- name: list_tables
|
|
20
|
+
description: List all tables in a database.
|
|
21
|
+
parameters:
|
|
22
|
+
- name: connection_string
|
|
23
|
+
type: string
|
|
24
|
+
required: true
|
|
25
|
+
- name: describe_table
|
|
26
|
+
description: 'Describe a table''s schema: column names, types, nullability, defaults,'
|
|
27
|
+
parameters:
|
|
28
|
+
- name: connection_string
|
|
29
|
+
type: string
|
|
30
|
+
required: true
|
|
31
|
+
- name: table_name
|
|
32
|
+
type: string
|
|
33
|
+
required: true
|
|
34
|
+
- name: insert_row
|
|
35
|
+
description: Insert a single row into a table. Column names and values are passed
|
|
36
|
+
parameters:
|
|
37
|
+
- name: connection_string
|
|
38
|
+
type: string
|
|
39
|
+
required: true
|
|
40
|
+
- name: table_name
|
|
41
|
+
type: string
|
|
42
|
+
required: true
|
|
43
|
+
- name: data
|
|
44
|
+
type: object
|
|
45
|
+
required: true
|
|
46
|
+
- name: export_to_csv
|
|
47
|
+
description: Execute a SELECT query and export results to a CSV file.
|
|
48
|
+
parameters:
|
|
49
|
+
- name: connection_string
|
|
50
|
+
type: string
|
|
51
|
+
required: true
|
|
52
|
+
- name: sql
|
|
53
|
+
type: string
|
|
54
|
+
required: true
|
|
55
|
+
- name: output_path
|
|
56
|
+
type: string
|
|
57
|
+
required: false
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
# Ensure shared auth middleware is available
|
|
6
|
+
sys.path.insert(0, os.path.expanduser("~/clawd/meok-labs-engine/shared"))
|
|
7
|
+
os.chdir(os.path.dirname(os.path.abspath(__file__)) + "/..")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestMCPImport(unittest.TestCase):
|
|
11
|
+
def test_import_server(self):
|
|
12
|
+
"""Server module must import without errors."""
|
|
13
|
+
import server # noqa: F401
|
|
14
|
+
|
|
15
|
+
def test_mcp_or_server_object_exists(self):
|
|
16
|
+
"""FastMCP servers export 'mcp'; low-level servers export 'server'."""
|
|
17
|
+
import server as srv
|
|
18
|
+
self.assertTrue(
|
|
19
|
+
hasattr(srv, "mcp") or hasattr(srv, "server"),
|
|
20
|
+
"Expected 'mcp' or 'server' object in server.py",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestAuthMiddleware(unittest.TestCase):
|
|
25
|
+
def test_check_access_blocks_empty_key(self):
|
|
26
|
+
"""Empty API key should be rejected on free tier."""
|
|
27
|
+
from auth_middleware import check_access
|
|
28
|
+
allowed, msg, tier = check_access("")
|
|
29
|
+
self.assertFalse(allowed)
|
|
30
|
+
self.assertIn("upgrade", msg.lower() or tier.lower())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestHealthEndpoint(unittest.TestCase):
|
|
34
|
+
def test_health_url_resolves(self):
|
|
35
|
+
"""Wrapper must expose /health."""
|
|
36
|
+
import urllib.request
|
|
37
|
+
# Note: this test requires the wrapper to be running on port 8000.
|
|
38
|
+
# It is skipped in CI unless the server is active.
|
|
39
|
+
try:
|
|
40
|
+
resp = urllib.request.urlopen("http://localhost:8000/health", timeout=2)
|
|
41
|
+
self.assertEqual(resp.status, 200)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
self.skipTest(f"Server not running: {e}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
unittest.main()
|