#
tokens: 5705/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .python-version
├── LICENSE
├── package.json
├── pyproject.toml
├── README.md
├── src
│   └── mcp_server_oracle
│       ├── __init__.py
│       └── oracle_tools.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.12
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | MANIFEST
 28 | 
 29 | # PyInstaller
 30 | #  Usually these files are written by a python script from a template
 31 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 32 | *.manifest
 33 | *.spec
 34 | 
 35 | # Installer logs
 36 | pip-log.txt
 37 | pip-delete-this-directory.txt
 38 | 
 39 | # Unit test / coverage reports
 40 | htmlcov/
 41 | .tox/
 42 | .nox/
 43 | .coverage
 44 | .coverage.*
 45 | .cache
 46 | nosetests.xml
 47 | coverage.xml
 48 | *.cover
 49 | *.py,cover
 50 | .hypothesis/
 51 | .pytest_cache/
 52 | cover/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | db.sqlite3
 62 | db.sqlite3-journal
 63 | 
 64 | # Flask stuff:
 65 | instance/
 66 | .webassets-cache
 67 | 
 68 | # Scrapy stuff:
 69 | .scrapy
 70 | 
 71 | # Sphinx documentation
 72 | docs/_build/
 73 | 
 74 | # PyBuilder
 75 | .pybuilder/
 76 | target/
 77 | 
 78 | # Jupyter Notebook
 79 | .ipynb_checkpoints
 80 | 
 81 | # IPython
 82 | profile_default/
 83 | ipython_config.py
 84 | 
 85 | # pyenv
 86 | #   For a library or package, you might want to ignore these files since the code is
 87 | #   intended to run in multiple environments; otherwise, check them in:
 88 | # .python-version
 89 | 
 90 | # pipenv
 91 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 92 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 93 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 94 | #   install all needed dependencies.
 95 | #Pipfile.lock
 96 | 
 97 | # UV
 98 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 99 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
100 | #   commonly ignored for libraries.
101 | #uv.lock
102 | 
103 | # poetry
104 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
106 | #   commonly ignored for libraries.
107 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 | 
110 | # pdm
111 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | #   in version control.
115 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 | 
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 | 
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 | 
127 | # SageMath parsed files
128 | *.sage.py
129 | 
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 | 
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 | 
143 | # Rope project settings
144 | .ropeproject
145 | 
146 | # mkdocs documentation
147 | /site
148 | 
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 | 
154 | # Pyre type checker
155 | .pyre/
156 | 
157 | # pytype static type analyzer
158 | .pytype/
159 | 
160 | # Cython debug symbols
161 | cython_debug/
162 | 
163 | # PyCharm
164 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
167 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 | 
170 | # Ruff stuff:
171 | .ruff_cache/
172 | 
173 | # PyPI configuration file
174 | .pypirc
175 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # mcp-server-oracle
 2 | Model Context Protocol server to access oracle
 3 | 
 4 | [![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/)
 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
 6 | 
 7 | ## Demos
 8 | 
 9 | 
10 | https://github.com/user-attachments/assets/dc4e377b-4efb-43e6-85fa-93ed852fe21f
11 | 
12 | 
13 | 
14 | ## Quickstart
15 | 
16 | To try this in Claude Desktop app, add this to your claude config files:
17 | 
18 | ```json
19 | {
20 |   "mcpServers": {
21 |     "mcp-server-oracle": {
22 |       "command": "uvx",
23 |       "args": [
24 |         "mcp-server-oracle"
25 |       ],
26 |       "env": {
27 |         "ORACLE_CONNECTION_STRING": "username/password@hostname:port/service_name"
28 |       }
29 |     }
30 |   }
31 | }
32 | ```
33 | 
34 | ### Prerequisites
35 | 
36 | - UV (pacakge manager)
37 | - Python 3.12+
38 | - Claude Desktop
39 | 
40 | ### Installation
41 | 
42 | #### Claude Desktop Configuration
43 | 
44 | Add the server configuration to your Claude Desktop config file:
45 | 
46 | **MacOS**: `~/Library/Application\ Support/Claude/claude_desktop_config.json`  
47 | **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
48 | 
49 | 
50 | ## Contributing
51 | 
52 | 1. Fork the repository from [mcp-server-oracle](https://github.com/hdcola/mcp-server-oracle)
53 | 2. Create your feature branch
54 | 3. Commit your changes
55 | 4. Push to the branch
56 | 5. Open a Pull Request
57 | 
58 | ## License
59 | 
60 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
61 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "mcp-server-oracle",
 3 |   "version": "1.0.1",
 4 |   "description": "",
 5 |   "main": "index.js",
 6 |   "scripts": {
 7 |     "inspector": "npx @modelcontextprotocol/inspector uv run main.py"
 8 |   },
 9 |   "keywords": [],
10 |   "author": "hdcola",
11 |   "license": "MIT",
12 |   "packageManager": "[email protected]"
13 | }
14 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "mcp-server-oracle"
 3 | version = "1.0.2"
 4 | description = "Model Context Protocol server to access oracle database"
 5 | authors = [
 6 |     { name = "hdcola", email = "[email protected]" }
 7 | ]
 8 | readme = "README.md"
 9 | requires-python = ">=3.12"
10 | dependencies = [
11 |     "mcp[cli]>=1.5.0",
12 |     "oracledb>=3.0.0",
13 |     "python-dotenv>=1.0.1",
14 | ]
15 | 
16 | [project.scripts]
17 | mcp-server-oracle = "mcp_server_oracle:main"
18 | dev = "mcp_server_oracle:dev"
19 | 
20 | [build-system]
21 | requires = ["hatchling"]
22 | build-backend = "hatchling.build"
23 | 
```

--------------------------------------------------------------------------------
/src/mcp_server_oracle/__init__.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | import sys
  3 | import signal
  4 | from typing import Any
  5 | from mcp.server.fastmcp import FastMCP
  6 | from . import oracle_tools
  7 | from dotenv import load_dotenv
  8 | 
  9 | 
 10 | # Load the environment variables
 11 | load_dotenv()
 12 | 
 13 | # Initialize the FastMCP server
 14 | mcp = FastMCP("mcp-server-oracle")
 15 | 
 16 | oracle_tools.connection_string = os.getenv("ORACLE_CONNECTION_STRING")
 17 | 
 18 | 
 19 | @mcp.tool()
 20 | async def list_tables() -> str:
 21 |     """Get a list of all tables in the oracle database
 22 | 
 23 |     Args:
 24 |         None
 25 |     """
 26 |     return await oracle_tools.list_tables()
 27 | 
 28 | 
 29 | @mcp.tool()
 30 | async def describe_table(table_name: str) -> str:
 31 |     """Get a description of a table in the oracle database"
 32 | 
 33 |     Args:
 34 |         table_name (string): The name of the table to describe
 35 |     """
 36 |     return await oracle_tools.describe_table(table_name)
 37 | 
 38 | 
 39 | @mcp.tool()
 40 | async def read_query(query: str) -> str:
 41 |     """Execute SELECT queries to read data from the oracle database
 42 | 
 43 |     Args:
 44 |         query (string): The SELECT query to execute
 45 |     """
 46 |     return await oracle_tools.read_query(query)
 47 | 
 48 | @mcp.tool()
 49 | async def exec_dml_sql(execsql: str) -> str:
 50 |     """Execute insert/update/delete/truncate to the oracle database
 51 | 
 52 |     Args:
 53 |         query (string): The sql to execute
 54 |     """
 55 |     return await oracle_tools.exec_dml_sql(execsql)
 56 | 
 57 | @mcp.tool()
 58 | async def exec_ddl_sql(execsql: str) -> str:
 59 |     """Execute create/drop/alter to the oracle database
 60 | 
 61 |     Args:
 62 |         query (string): The sql to execute
 63 |     """
 64 |     return await oracle_tools.exec_ddl_sql(execsql)
 65 | 
 66 | @mcp.tool()
 67 | async def exec_pro_sql(execsql: str) -> str:
 68 |     """Execute PL/SQL code blocks including stored procedures, functions and anonymous blocks
 69 | 
 70 |     Args:
 71 |         execsql (string): The PL/SQL code block to execute
 72 |     """
 73 |     return await oracle_tools.exec_pro_sql(execsql)
 74 | 
 75 | 
 76 | def main() -> None:
 77 |     mcp.run(transport='stdio')
 78 | 
 79 | 
 80 | def dev() -> None:
 81 |     """
 82 |     Development function that handles Ctrl+C gracefully.
 83 |     This function calls main() but catches KeyboardInterrupt to allow 
 84 |     clean exit when user presses Ctrl+C.
 85 |     """
 86 |     print("mcp server starting", file=sys.stderr)
 87 | 
 88 |     # Define signal handler for cleaner exit
 89 |     def signal_handler(sig, frame):
 90 |         print("\nShutting down mcp server...", file=sys.stderr)
 91 |         sys.exit(0)
 92 | 
 93 |     # Register the signal handler for SIGINT (Ctrl+C)
 94 |     signal.signal(signal.SIGINT, signal_handler)
 95 | 
 96 |     try:
 97 |         # Run the server with proper exception handling
 98 |         main()
 99 |     except KeyboardInterrupt:
100 |         print("\nShutting down mcp server...", file=sys.stderr)
101 |         sys.exit(0)
102 |     except Exception as e:
103 |         print(f"\nError: {e}", file=sys.stderr)
104 |         sys.exit(1)
105 | 
```

--------------------------------------------------------------------------------
/src/mcp_server_oracle/oracle_tools.py:
--------------------------------------------------------------------------------

```python
  1 | import oracledb
  2 | import asyncio
  3 | 
  4 | connection_string = ""
  5 | 
  6 | # 初始化为 Thin 模式
  7 | oracledb.init_oracle_client(lib_dir=None)
  8 | 
  9 | async def list_tables() -> list:
 10 |     tables = []
 11 |     try:
 12 |         # Run database operations in a separate thread
 13 |         def db_operation():
 14 |             result_tables = []
 15 |             with oracledb.connect(connection_string) as conn:
 16 |                 cursor = conn.cursor()
 17 |                 cursor.execute(
 18 |                     "SELECT table_name FROM user_tables ORDER BY table_name")
 19 |                 for row in cursor:
 20 |                     result_tables.append(row[0])
 21 |             return '\n'.join(result_tables)
 22 | 
 23 |         return await asyncio.to_thread(db_operation)
 24 |     except oracledb.DatabaseError as e:
 25 |         print('Error occurred:', e)
 26 |         return str(e)
 27 | 
 28 | 
 29 | async def describe_table(table_name: str) -> str:
 30 |     try:
 31 |         # Run database operations in a separate thread
 32 |         def db_operation(table):
 33 |             with oracledb.connect(connection_string) as conn:
 34 |                 cursor = conn.cursor()
 35 | 
 36 |                 # Create CSV headers
 37 |                 result = [
 38 |                     "COLUMN_NAME,DATA_TYPE,NULLABLE,DATA_LENGTH,PRIMARY_KEY,FOREIGN_KEY"]
 39 | 
 40 |                 # Get primary key columns
 41 |                 pk_columns = []
 42 |                 cursor.execute(
 43 |                     """
 44 |                     SELECT cols.column_name
 45 |                     FROM all_constraints cons, all_cons_columns cols
 46 |                     WHERE cons.constraint_type = 'P'
 47 |                     AND cons.constraint_name = cols.constraint_name
 48 |                     AND cons.owner = cols.owner
 49 |                     AND cols.table_name = :table_name
 50 |                     """,
 51 |                     table_name=table.upper()
 52 |                 )
 53 |                 for row in cursor:
 54 |                     pk_columns.append(row[0])
 55 | 
 56 |                 # Get foreign key columns and references
 57 |                 fk_info = {}
 58 |                 cursor.execute(
 59 |                     """
 60 |                     SELECT a.column_name, c_pk.table_name as referenced_table, b.column_name as referenced_column
 61 |                     FROM all_cons_columns a
 62 |                     JOIN all_constraints c ON a.owner = c.owner AND a.constraint_name = c.constraint_name
 63 |                     JOIN all_constraints c_pk ON c.r_owner = c_pk.owner AND c.r_constraint_name = c_pk.constraint_name
 64 |                     JOIN all_cons_columns b ON c_pk.owner = b.owner AND c_pk.constraint_name = b.constraint_name
 65 |                     WHERE c.constraint_type = 'R'
 66 |                     AND a.table_name = :table_name
 67 |                     """,
 68 |                     table_name=table.upper()
 69 |                 )
 70 |                 for row in cursor:
 71 |                     fk_info[row[0]] = f"{row[1]}.{row[2]}"
 72 | 
 73 |                 # Get main column information
 74 |                 cursor.execute(
 75 |                     """
 76 |                     SELECT column_name, data_type, nullable, data_length 
 77 |                     FROM user_tab_columns 
 78 |                     WHERE table_name = :table_name 
 79 |                     ORDER BY column_id
 80 |                     """,
 81 |                     table_name=table.upper()
 82 |                 )
 83 | 
 84 |                 rows_found = False
 85 |                 for row in cursor:
 86 |                     rows_found = True
 87 |                     column_name = row[0]
 88 |                     data_type = row[1]
 89 |                     nullable = row[2]
 90 |                     data_length = str(row[3])
 91 |                     is_pk = "YES" if column_name in pk_columns else "NO"
 92 |                     fk_ref = fk_info.get(column_name, "NO")
 93 | 
 94 |                     # Format as CSV row
 95 |                     result.append(
 96 |                         f"{column_name},{data_type},{nullable},{data_length},{is_pk},{fk_ref}")
 97 | 
 98 |                 if not rows_found:
 99 |                     return f"Table {table} not found or has no columns."
100 | 
101 |                 return '\n'.join(result)
102 | 
103 |         return await asyncio.to_thread(db_operation, table_name)
104 |     except oracledb.DatabaseError as e:
105 |         print('Error occurred:', e)
106 |         return str(e)
107 | 
108 | 
109 | async def read_query(query: str) -> str:
110 |     try:
111 |         # Check if the query is a SELECT statement
112 |         if not query.strip().upper().startswith('SELECT'):
113 |             return "Error: Only SELECT statements are supported."
114 | 
115 |         # Run database operations in a separate thread
116 |         def db_operation(query):
117 |             with oracledb.connect(connection_string) as conn:
118 |                 cursor = conn.cursor()
119 |                 cursor.execute(query)  # Execute query first
120 | 
121 |                 # Get column names after executing the query
122 |                 columns = [col[0] for col in cursor.description]
123 |                 result = [','.join(columns)]  # Add column headers
124 | 
125 |                 # Process each row
126 |                 for row in cursor:
127 |                     # Convert each value in the tuple to string
128 |                     string_values = [
129 |                         str(val) if val is not None else "NULL" for val in row]
130 |                     result.append(','.join(string_values))
131 | 
132 |                 return '\n'.join(result)
133 | 
134 |         return await asyncio.to_thread(db_operation, query)
135 |     except oracledb.DatabaseError as e:
136 |         print('Error occurred:', e)
137 |         return str(e)
138 | 
139 | 
140 | async def exec_dml_sql(execsql: str) -> str:
141 |     try:
142 |         # 检查SQL语句是否包含DML关键字
143 |         sql_upper = execsql.upper()
144 |         if not any(keyword in sql_upper for keyword in ['INSERT', 'DELETE', 'TRUNCATE', 'UPDATE']):
145 |             return "Error: Only INSERT, DELETE, TRUNCATE or UPDATE statements are supported."
146 |         
147 |         # Run database operations in a separate thread
148 |         def db_operation(query):
149 |             with oracledb.connect(connection_string) as conn:
150 |                 cursor = conn.cursor()
151 |                 # 执行DML语句
152 |                 cursor.execute(query)
153 |                 # 获取影响的行数
154 |                 rows_affected = cursor.rowcount
155 |                 # 提交事务
156 |                 conn.commit()
157 |                 # 返回执行结果
158 |                 return f"执行成功: 影响了 {rows_affected} 行数据"
159 | 
160 |         return await asyncio.to_thread(db_operation, execsql)
161 |     except oracledb.DatabaseError as e:
162 |         print('Error occurred:', e)
163 |         return str(e)
164 | 
165 | async def exec_ddl_sql(execsql: str) -> str:
166 |     try:
167 |         # 检查SQL语句是否包含ddl关键字
168 |         sql_upper = execsql.upper()
169 |         if not any(keyword in sql_upper for keyword in ['CREATE', 'ALTER', 'DROP']):
170 |             return "Error: Only CREATE, ALTER, DROP statements are supported."
171 |         
172 |         def db_operation(query):
173 |             with oracledb.connect(connection_string) as conn:
174 |                 cursor = conn.cursor()
175 |                 cursor.execute(query)
176 |                 return "DDL语句执行成功"
177 | 
178 |         return await asyncio.to_thread(db_operation, execsql)
179 |     except oracledb.DatabaseError as e:
180 |         print('Error occurred:', e)
181 |         return str(e)
182 | 
183 | async def exec_pro_sql(execsql: str) -> str:
184 |     try:
185 |         # Run database operations in a separate thread
186 |         def db_operation(query):
187 |             with oracledb.connect(connection_string) as conn:
188 |                 cursor = conn.cursor()
189 |                 # 执行PL/SQL代码块
190 |                 cursor.execute(query)
191 |                 # 如果有输出参数或返回值,尝试获取
192 |                 try:
193 |                     result = cursor.fetchall()
194 |                     if result:
195 |                         # 将结果格式化为字符串
196 |                         return '\n'.join(','.join(str(col) if col is not None else 'NULL' for col in row) for row in result)
197 |                 except oracledb.DatabaseError:
198 |                     # 如果没有结果集,说明是存储过程或无返回值的PL/SQL块
199 |                     pass
200 |                 # 提交事务
201 |                 conn.commit()
202 |                 return "PL/SQL代码块执行成功"
203 | 
204 |         return await asyncio.to_thread(db_operation, execsql)
205 |     except oracledb.DatabaseError as e:
206 |         print('Error occurred:', e)
207 |         return str(e)
208 |     except oracledb.DatabaseError as e:
209 |         print('Error occurred:', e)
210 |         return str(e)
211 | 
212 | if __name__ == "__main__":
213 |     # Create and run the async event loop
214 |     async def main():
215 |         # print(await list_tables())
216 |         print(await describe_table('CONCAT'))
217 | 
218 |     asyncio.run(main())
219 | 
```