sql-mcp-server 0.1.0__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.
db.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pyodbc
|
|
2
|
+
import os
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Force load .env from the project folder
|
|
7
|
+
load_dotenv(dotenv_path=Path(__file__).parent / ".env")
|
|
8
|
+
|
|
9
|
+
# db.py — support both auth modes
|
|
10
|
+
def get_connection():
|
|
11
|
+
driver = os.getenv("DB_DRIVER", "ODBC Driver 17 for SQL Server")
|
|
12
|
+
server = os.getenv("DB_SERVER")
|
|
13
|
+
database = os.getenv("DB_NAME")
|
|
14
|
+
username = os.getenv("DB_USER") # new
|
|
15
|
+
password = os.getenv("DB_PASSWORD") # new
|
|
16
|
+
|
|
17
|
+
if username and password:
|
|
18
|
+
conn_str = (
|
|
19
|
+
f"DRIVER={{{driver}}};SERVER={server};DATABASE={database};"
|
|
20
|
+
f"UID={username};PWD={password};TrustServerCertificate=yes;"
|
|
21
|
+
)
|
|
22
|
+
else:
|
|
23
|
+
conn_str = (
|
|
24
|
+
f"DRIVER={{{driver}}};SERVER={server};DATABASE={database};"
|
|
25
|
+
"Trusted_Connection=yes;TrustServerCertificate=yes;"
|
|
26
|
+
)
|
|
27
|
+
return pyodbc.connect(conn_str)
|
server.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from mcp.server.fastmcp import FastMCP
|
|
2
|
+
from db import get_connection
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
# Initialize FastMCP server
|
|
7
|
+
mcp = FastMCP("sql-assistant")
|
|
8
|
+
|
|
9
|
+
# ─────────────────────────────────────────────
|
|
10
|
+
# Helpers
|
|
11
|
+
# ─────────────────────────────────────────────
|
|
12
|
+
FORBIDDEN_KEYWORDS = ["INSERT", "UPDATE", "DELETE", "DROP", "TRUNCATE", "ALTER", "CREATE", "EXEC"]
|
|
13
|
+
|
|
14
|
+
def _check_select_only(sql: str) -> str | None:
|
|
15
|
+
"""Return an error message if `sql` contains a forbidden keyword, else None."""
|
|
16
|
+
sql_upper = sql.strip().upper()
|
|
17
|
+
for keyword in FORBIDDEN_KEYWORDS:
|
|
18
|
+
if sql_upper.startswith(keyword) or f" {keyword} " in sql_upper:
|
|
19
|
+
return f"❌ '{keyword}' statements are not allowed. Only SELECT queries are permitted."
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def _validate_identifier(name: str) -> bool:
|
|
23
|
+
"""Return True if `name` is a safe SQL identifier (letters, digits, underscores)."""
|
|
24
|
+
return bool(re.match(r'^[a-zA-Z0-9_]+$', name))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ─────────────────────────────────────────────
|
|
28
|
+
# TOOL 1: list_tables
|
|
29
|
+
# ─────────────────────────────────────────────
|
|
30
|
+
@mcp.tool()
|
|
31
|
+
def list_tables() -> str:
|
|
32
|
+
"""
|
|
33
|
+
List all user-created tables in the connected MS SQL database.
|
|
34
|
+
Returns table names along with their schema.
|
|
35
|
+
"""
|
|
36
|
+
query = """
|
|
37
|
+
SELECT TABLE_SCHEMA, TABLE_NAME
|
|
38
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
39
|
+
WHERE TABLE_TYPE = 'BASE TABLE'
|
|
40
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
conn = get_connection()
|
|
44
|
+
cursor = conn.cursor()
|
|
45
|
+
cursor.execute(query)
|
|
46
|
+
rows = cursor.fetchall()
|
|
47
|
+
conn.close()
|
|
48
|
+
|
|
49
|
+
if not rows:
|
|
50
|
+
return "No tables found in the database."
|
|
51
|
+
|
|
52
|
+
result = "Tables in database:\n\n"
|
|
53
|
+
for schema, table in rows:
|
|
54
|
+
result += f" [{schema}].[{table}]\n"
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
return f"Error fetching tables: {str(e)}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ─────────────────────────────────────────────
|
|
62
|
+
# TOOL 2: describe_table
|
|
63
|
+
# ─────────────────────────────────────────────
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
def describe_table(table_name: str, schema: str = "dbo") -> str:
|
|
66
|
+
"""
|
|
67
|
+
Describe the structure of a specific table — columns, data types, nullability.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
table_name: Name of the table (e.g. Customers)
|
|
71
|
+
schema: Schema name, default is 'dbo'
|
|
72
|
+
"""
|
|
73
|
+
query = """
|
|
74
|
+
SELECT
|
|
75
|
+
COLUMN_NAME,
|
|
76
|
+
DATA_TYPE,
|
|
77
|
+
CHARACTER_MAXIMUM_LENGTH,
|
|
78
|
+
IS_NULLABLE,
|
|
79
|
+
COLUMN_DEFAULT
|
|
80
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
81
|
+
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?
|
|
82
|
+
ORDER BY ORDINAL_POSITION
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
conn = get_connection()
|
|
86
|
+
cursor = conn.cursor()
|
|
87
|
+
cursor.execute(query, (table_name, schema))
|
|
88
|
+
rows = cursor.fetchall()
|
|
89
|
+
conn.close()
|
|
90
|
+
|
|
91
|
+
if not rows:
|
|
92
|
+
return f"Table [{schema}].[{table_name}] not found or has no columns."
|
|
93
|
+
|
|
94
|
+
result = f"Structure of [{schema}].[{table_name}]:\n\n"
|
|
95
|
+
|
|
96
|
+
for col_name, data_type, max_len, nullable, default in rows:
|
|
97
|
+
max_len = max_len if max_len else "-"
|
|
98
|
+
default = default if default else "-"
|
|
99
|
+
|
|
100
|
+
result += f"{col_name} | {data_type} | {max_len} | {nullable} | {default}\n"
|
|
101
|
+
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return f"Error describing table: {str(e)}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ─────────────────────────────────────────────
|
|
109
|
+
# TOOL 3: run_query
|
|
110
|
+
# ─────────────────────────────────────────────
|
|
111
|
+
@mcp.tool()
|
|
112
|
+
def run_query(sql: str, max_rows: int = 50) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Run a SELECT SQL query on the MS SQL database and return results.
|
|
115
|
+
Only SELECT statements are allowed for safety.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
sql: A valid T-SQL SELECT query
|
|
119
|
+
max_rows: Maximum rows to return (default 50, max 200)
|
|
120
|
+
"""
|
|
121
|
+
# Safety check — block destructive statements
|
|
122
|
+
err = _check_select_only(sql)
|
|
123
|
+
if err:
|
|
124
|
+
return err
|
|
125
|
+
|
|
126
|
+
max_rows = min(max_rows, 200) # hard cap
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
conn = get_connection()
|
|
130
|
+
cursor = conn.cursor()
|
|
131
|
+
cursor.execute(sql)
|
|
132
|
+
rows = cursor.fetchmany(max_rows)
|
|
133
|
+
columns = [desc[0] for desc in cursor.description]
|
|
134
|
+
conn.close()
|
|
135
|
+
|
|
136
|
+
if not rows:
|
|
137
|
+
return "Query executed successfully. No rows returned."
|
|
138
|
+
|
|
139
|
+
# Format as a readable table
|
|
140
|
+
col_widths = [max(len(str(col)), max((len(str(row[i])) for row in rows), default=0)) for i, col in enumerate(columns)]
|
|
141
|
+
|
|
142
|
+
header = " | ".join(str(col).ljust(col_widths[i]) for i, col in enumerate(columns))
|
|
143
|
+
separator = "-+-".join("-" * w for w in col_widths)
|
|
144
|
+
result_lines = [header, separator]
|
|
145
|
+
|
|
146
|
+
for row in rows:
|
|
147
|
+
line = " | ".join(str(val).ljust(col_widths[i]) for i, val in enumerate(row))
|
|
148
|
+
result_lines.append(line)
|
|
149
|
+
|
|
150
|
+
result_lines.append(f"\n({len(rows)} rows returned)")
|
|
151
|
+
return "\n".join(result_lines)
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return f"Query error: {str(e)}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ─────────────────────────────────────────────
|
|
158
|
+
# TOOL 4: explain_query
|
|
159
|
+
# ─────────────────────────────────────────────
|
|
160
|
+
@mcp.tool()
|
|
161
|
+
def explain_query(sql: str) -> str:
|
|
162
|
+
"""
|
|
163
|
+
Show the estimated execution plan for a SELECT query using SET SHOWPLAN_TEXT.
|
|
164
|
+
Helps understand how SQL Server processes the query.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
sql: A valid T-SQL SELECT query to explain
|
|
168
|
+
"""
|
|
169
|
+
# Safety check — same rules as run_query
|
|
170
|
+
err = _check_select_only(sql)
|
|
171
|
+
if err:
|
|
172
|
+
return err
|
|
173
|
+
|
|
174
|
+
conn = None
|
|
175
|
+
try:
|
|
176
|
+
conn = get_connection()
|
|
177
|
+
cursor = conn.cursor()
|
|
178
|
+
|
|
179
|
+
cursor.execute("SET SHOWPLAN_TEXT ON")
|
|
180
|
+
cursor.execute(sql)
|
|
181
|
+
rows = cursor.fetchall()
|
|
182
|
+
|
|
183
|
+
if not rows:
|
|
184
|
+
return "No execution plan returned."
|
|
185
|
+
|
|
186
|
+
plan_lines = [str(row[0]) for row in rows]
|
|
187
|
+
return "Execution Plan:\n\n" + "\n".join(plan_lines)
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
return f"Error fetching execution plan: {str(e)}"
|
|
191
|
+
finally:
|
|
192
|
+
if conn:
|
|
193
|
+
try:
|
|
194
|
+
conn.cursor().execute("SET SHOWPLAN_TEXT OFF")
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
conn.close()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ─────────────────────────────────────────────
|
|
201
|
+
# TOOL 5: get_table_sample
|
|
202
|
+
# ─────────────────────────────────────────────
|
|
203
|
+
@mcp.tool()
|
|
204
|
+
def get_table_sample(table_name: str, schema: str = "dbo", rows: int = 5) -> str:
|
|
205
|
+
"""
|
|
206
|
+
Preview the first N rows of any table quickly.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
table_name: Name of the table
|
|
210
|
+
schema: Schema name, default is 'dbo'
|
|
211
|
+
rows: Number of rows to preview (default 5)
|
|
212
|
+
"""
|
|
213
|
+
if not _validate_identifier(table_name):
|
|
214
|
+
return f"❌ Invalid table name: '{table_name}'. Only letters, digits, and underscores are allowed."
|
|
215
|
+
if not _validate_identifier(schema):
|
|
216
|
+
return f"❌ Invalid schema name: '{schema}'. Only letters, digits, and underscores are allowed."
|
|
217
|
+
|
|
218
|
+
sql = f"SELECT TOP {min(rows, 20)} * FROM [{schema}].[{table_name}]"
|
|
219
|
+
return run_query(sql)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ─────────────────────────────────────────────
|
|
223
|
+
# Entry Point
|
|
224
|
+
# ─────────────────────────────────────────────
|
|
225
|
+
def main():
|
|
226
|
+
mcp.run(transport="stdio")
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
main()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sql-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that connects Claude to a Microsoft SQL Server database
|
|
5
|
+
Author: Snehangshu
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: ai,claude,database,mcp,sql-server
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: mcp[cli]>=1.26.0
|
|
17
|
+
Requires-Dist: pyodbc>=5.3.0
|
|
18
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# SQL Assistant MCP Server
|
|
22
|
+
|
|
23
|
+
An MCP server that connects Claude to a Microsoft SQL Server database over stdio. Ask Claude to query your database, check table structures, or see how a query runs — without leaving the chat window.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install sql-mcp-server
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You also need the **ODBC Driver for SQL Server** installed:
|
|
34
|
+
https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Tools
|
|
39
|
+
|
|
40
|
+
**list_tables** — Lists every user-created table in the database, grouped by schema.
|
|
41
|
+
|
|
42
|
+
**describe_table** — Shows columns, data types, nullability, and defaults for a table.
|
|
43
|
+
|
|
44
|
+
**run_query** — Runs a SELECT query and returns results. Write operations (INSERT, UPDATE, DELETE, DROP, etc.) are blocked.
|
|
45
|
+
|
|
46
|
+
**explain_query** — Returns SQL Server's estimated execution plan via `SET SHOWPLAN_TEXT`. Useful before running a slow or unfamiliar query.
|
|
47
|
+
|
|
48
|
+
**get_table_sample** — Returns the first N rows (up to 20) from a table. Default is 5 — pass `rows=10` for more.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Project structure
|
|
53
|
+
```
|
|
54
|
+
sql-mcp-server/
|
|
55
|
+
├── server.py # MCP server and tool definitions
|
|
56
|
+
├── db.py # SQL Server connection helper (loads .env)
|
|
57
|
+
├── pyproject.toml # Project metadata and dependencies
|
|
58
|
+
├── uv.lock # Locked dependency versions (uv)
|
|
59
|
+
├── .env # Connection config (not committed)
|
|
60
|
+
└── README.md
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Setup
|
|
66
|
+
|
|
67
|
+
### 1. Configure the database connection
|
|
68
|
+
|
|
69
|
+
Create a `.env` file in the project root:
|
|
70
|
+
|
|
71
|
+
**Windows Authentication (Trusted Connection):**
|
|
72
|
+
```env
|
|
73
|
+
DB_DRIVER=ODBC Driver 17 for SQL Server
|
|
74
|
+
DB_SERVER=YOUR_SERVER_NAME\\SQLEXPRESS
|
|
75
|
+
DB_NAME=your_database_name
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**SQL Server Authentication (username + password):**
|
|
79
|
+
```env
|
|
80
|
+
DB_DRIVER=ODBC Driver 17 for SQL Server
|
|
81
|
+
DB_SERVER=YOUR_SERVER_NAME\\SQLEXPRESS
|
|
82
|
+
DB_NAME=your_database_name
|
|
83
|
+
DB_USER=your_username
|
|
84
|
+
DB_PASSWORD=your_password
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If you're using the Claude Desktop config below, put your credentials in the `env` block — no `.env` file needed.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### 2. Register with Claude Desktop
|
|
92
|
+
|
|
93
|
+
Add this to `claude_desktop_config.json`:
|
|
94
|
+
|
|
95
|
+
**Windows Authentication:**
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"sql-assistant": {
|
|
100
|
+
"command": "uv",
|
|
101
|
+
"args": [
|
|
102
|
+
"--directory",
|
|
103
|
+
"C:/path/to/sql-mcp-server",
|
|
104
|
+
"run",
|
|
105
|
+
"server.py"
|
|
106
|
+
],
|
|
107
|
+
"env": {
|
|
108
|
+
"DB_SERVER": "YOUR_SERVER_NAME\\\\SQLEXPRESS",
|
|
109
|
+
"DB_NAME": "your_database_name",
|
|
110
|
+
"DB_DRIVER": "ODBC Driver 17 for SQL Server"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**SQL Server Authentication:**
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"sql-assistant": {
|
|
122
|
+
"command": "uv",
|
|
123
|
+
"args": [
|
|
124
|
+
"--directory",
|
|
125
|
+
"C:/path/to/sql-mcp-server",
|
|
126
|
+
"run",
|
|
127
|
+
"server.py"
|
|
128
|
+
],
|
|
129
|
+
"env": {
|
|
130
|
+
"DB_SERVER": "YOUR_SERVER_NAME\\\\SQLEXPRESS",
|
|
131
|
+
"DB_NAME": "your_database_name",
|
|
132
|
+
"DB_DRIVER": "ODBC Driver 17 for SQL Server",
|
|
133
|
+
"DB_USER": "your_username",
|
|
134
|
+
"DB_PASSWORD": "your_password"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Replace the placeholder values with your actual details. Restart Claude Desktop after saving.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Usage
|
|
146
|
+
|
|
147
|
+
With the server running, try asking Claude:
|
|
148
|
+
|
|
149
|
+
- "What tables are in the database?"
|
|
150
|
+
- "Describe the Orders table."
|
|
151
|
+
- "Show me 10 rows from Customers."
|
|
152
|
+
- "Run: SELECT TOP 5 * FROM Sales.Orders WHERE Status = 'Pending'"
|
|
153
|
+
- "Explain this query: SELECT * FROM Products WHERE Price > 100"
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Safety
|
|
158
|
+
|
|
159
|
+
Only SELECT queries run. These keywords are blocked before they reach the database:
|
|
160
|
+
|
|
161
|
+
`INSERT` `UPDATE` `DELETE` `DROP` `TRUNCATE` `ALTER` `CREATE` `EXEC`
|
|
162
|
+
|
|
163
|
+
Table and schema names are validated against injection patterns. Results cap at 200 rows.
|
|
164
|
+
|
|
165
|
+
> ⚠️ **Don't commit your `.env` file** — it contains your connection credentials.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Requirements
|
|
170
|
+
|
|
171
|
+
- Python 3.12+
|
|
172
|
+
- SQL Server with ODBC Driver 17 or 18
|
|
173
|
+
- Claude Desktop with MCP support
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
db.py,sha256=4qP9HX4IDamu7zqxSMiCQG_zOCPbIGFtNkrE68uXVio,916
|
|
2
|
+
server.py,sha256=JC9wI6WJ4ZzxUCal69WrNBOr_uvbt_740_Fxakwr_eM,8578
|
|
3
|
+
sql_mcp_server-0.1.0.dist-info/METADATA,sha256=HAsMJ-fsfi90eNxitTHQWv3lOv1qpGF_h2g6y4f4gGU,4557
|
|
4
|
+
sql_mcp_server-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
5
|
+
sql_mcp_server-0.1.0.dist-info/entry_points.txt,sha256=GJySdKWFd3S7nJoNcbECz1Nz8teN-aJBrJH9AbIySgM,47
|
|
6
|
+
sql_mcp_server-0.1.0.dist-info/licenses/LICENSE,sha256=EbNPKITs5IpzocH7nlUejIW62m8GMwcQa7Atd_4nUWU,1094
|
|
7
|
+
sql_mcp_server-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Snehangshu Bhuin
|
|
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. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|