Skip to content

Commit 07cfb41

Browse files
Initial commit
0 parents  commit 07cfb41

File tree

14 files changed

+1013
-0
lines changed

14 files changed

+1013
-0
lines changed

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
2+
# XiYan MCP Server
3+
4+
A Model Context Protocol (MCP) server that enables secure interaction with MySQL databases. This server allows AI assistants to list tables, read data, and execute SQL queries through a controlled interface, making database exploration and analysis safer and more structured.
5+
6+
## Features
7+
- Fetch data by natural language throught XiYanSQL (https://github.com/XGenerationLab/XiYan-SQL)
8+
- List available MySQL tables as resources
9+
- Read table contents
10+
11+
## Installation
12+
13+
```bash
14+
pip install xiyan-mcp-server
15+
```
16+
17+
## Configuration
18+
19+
Set the following environment variables:
20+
21+
```bash
22+
MYSQL_HOST= # Database host
23+
MYSQL_PORT= # Optional: Database port (defaults to 3306 if not specified)
24+
MYSQL_USER=
25+
MYSQL_PASSWORD=
26+
MYSQL_DATABASE=
27+
MODEL_NAME=
28+
MODEL_KEY=
29+
MODEL_URL=
30+
```
31+
32+
## Usage
33+
34+
### With Claude Desktop
35+
36+
Add this to your `claude_desktop_config.json`:
37+
38+
```json
39+
{
40+
"mcpServers": {
41+
"xiyan": {
42+
"command": "uv",
43+
"args": [
44+
"--directory",
45+
"path/to/xiyan_mcp_server",
46+
"run",
47+
"xiyan_mcp_server"
48+
],
49+
"env": {
50+
"MYSQL_HOST": "localhost",
51+
"MYSQL_PORT": "3306",
52+
"MYSQL_USER": "your_username",
53+
"MYSQL_PASSWORD": "your_password",
54+
"MYSQL_DATABASE": "your_database",
55+
"MODEL_NAME": "your_model_name",
56+
"MODEL_URL": "your_model enpoint",
57+
"MODEL_KEY": "your_model_key"
58+
}
59+
}
60+
}
61+
}
62+
```
63+
64+
### As a standalone server
65+
66+
```bash
67+
# Install dependencies
68+
pip install -r requirements.txt
69+
70+
# Run the server
71+
python -m xiyan_mcp_server
72+
```
73+
74+
## Development
75+
76+
```bash
77+
# Clone the repository
78+
git clone https://github.com/yourusername/mysql_mcp_server.git
79+
cd xiyan_mcp_server
80+
81+
# Create virtual environment
82+
python -m venv venv
83+
source venv/bin/activate # or `venv\Scripts\activate` on Windows
84+
85+
# Install development dependencies
86+
pip install -r requirements.txt
87+
88+
```

pyproject.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[project]
2+
name = "xiyan_mcp_server"
3+
version = "0.1.0"
4+
description = "A Model Context Protocol (MCP) server that using XiyanSQL with MySQL databases. This server allows AI assistants to list tables, read data, and execute natual language queries."
5+
readme = "README.md"
6+
requires-python = ">=3.11"
7+
dependencies = [
8+
"mcp>=1.0.0",
9+
"mysql-connector-python>=9.1.0",
10+
"llama_index",
11+
"sqlalchemy"
12+
]
13+
[[project.authors]]
14+
name = "Zhiling Luo"
15+
email = "godot.lzl@alibaba-inc.com"
16+
17+
[build-system]
18+
requires = ["hatchling"]
19+
build-backend = "hatchling.build"
20+
21+
[project.scripts]
22+
mysql_mcp_server = "xiyan_mcp_server:main"

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
mcp>=1.0.0
2+
mysql-connector-python>=9.1.0
3+
sqlalchemy
4+
llama_index

src/xiyan_mcp_server/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#from . import server
2+
from server import *
3+
4+
def main():
5+
"""Main entry point for the package."""
6+
mcp.run()
7+
8+
# Expose important items at package level
9+
__all__ = ['main', 'server']
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from dataclasses import dataclass, field
2+
from typing import Optional
3+
4+
from urllib.parse import quote_plus
5+
@dataclass
6+
class DBConfig:
7+
dialect: str = 'sqlite'
8+
db_path: Optional[str] = None # 仅用于 SQLite
9+
db_name: Optional[str] = None # MySQL/PostgreSQL 通用
10+
user_name: Optional[str] = None # MySQL/PostgreSQL 通用
11+
db_pwd: Optional[str] = None # MySQL/PostgreSQL 通用
12+
db_host: Optional[str] = None # MySQL/PostgreSQL 通用
13+
port: Optional[int] = None # MySQL/PostgreSQL 通用
14+
15+
def __post_init__(self):
16+
if self.dialect == 'sqlite':
17+
self.db_path = self.db_path or 'book_1.sqlite'
18+
elif self.dialect in ['mysql', 'postgresql']:
19+
self.db_name = self.db_name or 'default_db'
20+
self.user_name = self.user_name or 'default_user'
21+
self.db_pwd = quote_plus(self.db_pwd) or 'default_password'
22+
self.db_host = self.db_host or 'localhost'
23+
self.port = self.port or (3306 if self.dialect == 'mysql' else 5432)
24+
else:
25+
raise ValueError(f"Unsupported database dialect: {self.dialect}")

src/xiyan_mcp_server/database_env.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from datasource.db_source import HITLSQLDatabase
2+
3+
class DataBaseEnv:
4+
def __init__(self, database: HITLSQLDatabase):
5+
self.database = database
6+
self.dialect = database.dialect
7+
self.mschema = database.mschema
8+
self.db_name = database.db_name
9+
self.mschema_str = self.mschema.to_mschema()
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import random
2+
from utils.file_util import read_json_file, write_json_to_file, save_raw_text
3+
from utils.db_util import examples_to_str
4+
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
5+
6+
7+
class MSchema:
8+
def __init__(self, db_id: str = 'Anonymous', schema: Optional[str] = None):
9+
self.db_id = db_id
10+
self.schema = schema
11+
self.tables = {}
12+
self.foreign_keys = []
13+
14+
def add_table(self, name, fields={}, comment=None):
15+
self.tables[name] = {"fields": fields.copy(), 'examples': [], 'comment': comment}
16+
17+
def add_field(self, table_name: str, field_name: str, field_type: str = "",
18+
primary_key: bool = False, nullable: bool = True, default: Any = None,
19+
autoincrement: bool = False, comment: str = "", examples: list = [], **kwargs):
20+
self.tables[table_name]["fields"][field_name] = {
21+
"type": field_type,
22+
"primary_key": primary_key,
23+
"nullable": nullable,
24+
"default": default if default is None else f'{default}',
25+
"autoincrement": autoincrement,
26+
"comment": comment,
27+
"examples": examples.copy(),
28+
**kwargs}
29+
30+
def add_foreign_key(self, table_name, field_name, ref_schema, ref_table_name, ref_field_name):
31+
self.foreign_keys.append([table_name, field_name, ref_schema, ref_table_name, ref_field_name])
32+
33+
def get_field_type(self, field_type, simple_mode=True)->str:
34+
if not simple_mode:
35+
return field_type
36+
else:
37+
return field_type.split("(")[0]
38+
39+
def has_table(self, table_name: str) -> bool:
40+
if table_name in self.tables.keys():
41+
return True
42+
else:
43+
return False
44+
45+
def has_column(self, table_name: str, field_name: str) -> bool:
46+
if self.has_table(table_name):
47+
if field_name in self.tables[table_name]["fields"].keys():
48+
return True
49+
else:
50+
return False
51+
else:
52+
return False
53+
54+
def get_field_info(self, table_name: str, field_name: str) -> Dict:
55+
try:
56+
return self.tables[table_name]['fields'][field_name]
57+
except:
58+
return {}
59+
60+
def single_table_mschema(self, table_name: str, selected_columns: List = None,
61+
example_num=3, show_type_detail=False, shuffle=True) -> str:
62+
table_info = self.tables.get(table_name, {})
63+
output = []
64+
table_comment = table_info.get('comment', '')
65+
if table_comment is not None and table_comment != 'None' and len(table_comment) > 0:
66+
if self.schema is not None and len(self.schema) > 0:
67+
output.append(f"# Table: {self.schema}.{table_name}, {table_comment}")
68+
else:
69+
output.append(f"# Table: {table_name}, {table_comment}")
70+
else:
71+
if self.schema is not None and len(self.schema) > 0:
72+
output.append(f"# Table: {self.schema}.{table_name}")
73+
else:
74+
output.append(f"# Table: {table_name}")
75+
76+
field_lines = []
77+
# 处理表中的每一个字段
78+
for field_name, field_info in table_info['fields'].items():
79+
if selected_columns is not None and field_name.lower() not in selected_columns:
80+
continue
81+
82+
raw_type = self.get_field_type(field_info['type'], not show_type_detail)
83+
field_line = f"({field_name}:{raw_type.upper()}"
84+
if field_info['comment'] != '':
85+
field_line += f", {field_info['comment'].strip()}"
86+
else:
87+
pass
88+
89+
## 打上主键标识
90+
is_primary_key = field_info.get('primary_key', False)
91+
if is_primary_key:
92+
field_line += f", Primary Key"
93+
94+
# 如果有示例,添加上
95+
if len(field_info.get('examples', [])) > 0 and example_num > 0:
96+
examples = field_info['examples']
97+
examples = [s for s in examples if s is not None]
98+
examples = examples_to_str(examples)
99+
if len(examples) > example_num:
100+
examples = examples[:example_num]
101+
102+
if raw_type in ['DATE', 'TIME', 'DATETIME', 'TIMESTAMP']:
103+
examples = [examples[0]]
104+
elif len(examples) > 0 and max([len(s) for s in examples]) > 20:
105+
if max([len(s) for s in examples]) > 50:
106+
examples = []
107+
else:
108+
examples = [examples[0]]
109+
else:
110+
pass
111+
if len(examples) > 0:
112+
example_str = ', '.join([str(example) for example in examples])
113+
field_line += f", Examples: [{example_str}]"
114+
else:
115+
pass
116+
else:
117+
field_line += ""
118+
field_line += ")"
119+
120+
field_lines.append(field_line)
121+
122+
if shuffle:
123+
random.shuffle(field_lines)
124+
125+
output.append('[')
126+
output.append(',\n'.join(field_lines))
127+
output.append(']')
128+
129+
return '\n'.join(output)
130+
131+
def to_mschema(self, selected_tables: List = None, selected_columns: List = None,
132+
example_num=3, show_type_detail=False, shuffle=True) -> str:
133+
"""
134+
convert to a MSchema string.
135+
selected_tables: 默认为None,表示选择所有的表
136+
selected_columns: 默认为None,表示所有列全选,格式['table_name.column_name']
137+
"""
138+
output = []
139+
140+
if selected_tables is not None:
141+
selected_tables = [s.lower() for s in selected_tables]
142+
if selected_columns is not None:
143+
selected_columns = [s.lower() for s in selected_columns]
144+
selected_tables = [s.split('.')[0].lower() for s in selected_columns]
145+
146+
# 依次处理每一个表
147+
for table_name, table_info in self.tables.items():
148+
if selected_tables is None or table_name.lower() in selected_tables:
149+
cur_table_type = table_info.get('type', 'table')
150+
column_names = list(table_info['fields'].keys())
151+
if selected_columns is not None:
152+
cur_selected_columns = [c for c in column_names if f"{table_name}.{c}".lower() in selected_columns]
153+
else:
154+
cur_selected_columns = selected_columns
155+
output.append(self.single_table_mschema(table_name, cur_selected_columns, example_num, show_type_detail, shuffle))
156+
157+
if shuffle:
158+
random.shuffle(output)
159+
160+
output.insert(0, f"【DB_ID】 {self.db_id}")
161+
output.insert(1, f"【Schema】")
162+
163+
# 添加外键信息,选择table_type为view时不展示外键
164+
if self.foreign_keys:
165+
output.append("【Foreign keys】")
166+
for fk in self.foreign_keys:
167+
ref_schema = fk[2]
168+
table1, column1, _, table2, column2 = fk
169+
if selected_tables is None or \
170+
(table1.lower() in selected_tables and table2.lower() in selected_tables):
171+
if ref_schema == self.schema:
172+
output.append(f"{fk[0]}.{fk[1]}={fk[3]}.{fk[4]}")
173+
174+
return '\n'.join(output)
175+
176+
def dump(self):
177+
schema_dict = {
178+
"db_id": self.db_id,
179+
"schema": self.schema,
180+
"tables": self.tables,
181+
"foreign_keys": self.foreign_keys
182+
}
183+
return schema_dict
184+
185+
def save(self, file_path: str):
186+
schema_dict = self.dump()
187+
write_json_to_file(file_path, schema_dict, is_json_line=False)
188+
189+
def load(self, file_path: str):
190+
data = read_json_file(file_path)
191+
self.db_id = data.get("db_id", "Anonymous")
192+
self.schema = data.get("schema", None)
193+
self.tables = data.get("tables", {})
194+
self.foreign_keys = data.get("foreign_keys", [])

0 commit comments

Comments
 (0)