strayl-mcp-server 0.1.3__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.
@@ -0,0 +1,3 @@
1
+ """Strayl MCP Server - MCP server for log search."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,12 @@
1
+ """Entry point for the Strayl MCP server."""
2
+
3
+ from .server import mcp
4
+
5
+
6
+ def main():
7
+ """Run the Strayl MCP server."""
8
+ mcp.run()
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,265 @@
1
+ """Strayl MCP Server - Log search tools."""
2
+
3
+ import os
4
+ from typing import Annotated, Optional
5
+ import httpx
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from .utils import parse_time_period, format_log_result
9
+
10
+ # Initialize FastMCP server
11
+ mcp = FastMCP(
12
+ "Strayl Log Search",
13
+ dependencies=[
14
+ "httpx>=0.27.0",
15
+ "python-dateutil>=2.8.0",
16
+ ]
17
+ )
18
+
19
+
20
+ # Strayl API base URL (hardcoded)
21
+ STRAYL_API_URL = "https://ougtygyvcgdnytkswier.supabase.co/functions/v1"
22
+
23
+
24
+ def get_api_key() -> str:
25
+ """Get API key from environment variable."""
26
+ api_key = os.getenv("STRAYL_API_KEY", "")
27
+
28
+ if not api_key:
29
+ raise ValueError(
30
+ "STRAYL_API_KEY environment variable is required. "
31
+ "Get your API key from https://strayl.dev"
32
+ )
33
+
34
+ return api_key
35
+
36
+
37
+ @mcp.tool()
38
+ async def search_logs_semantic(
39
+ query: Annotated[str, "Search query in natural language or keywords"],
40
+ time_period: Annotated[Optional[str], "Time filter: 5m, 1h, today, yesterday, 7d, 30d, etc."] = None,
41
+ match_threshold: Annotated[float, "Minimum similarity score (0.0 to 1.0)"] = 0.2,
42
+ match_count: Annotated[int, "Maximum number of results to return"] = 50,
43
+ ) -> str:
44
+ """Search logs using semantic (vector) search with optional time filtering.
45
+
46
+ This tool performs AI-powered semantic search across your logs, finding relevant entries
47
+ even if they don't contain exact keywords."""
48
+ try:
49
+ api_key = get_api_key()
50
+
51
+ # Parse time period if provided
52
+ start_time = None
53
+ end_time = None
54
+ if time_period:
55
+ start_time, end_time = parse_time_period(time_period)
56
+ if start_time is None:
57
+ return f"Error: Invalid time period '{time_period}'. Supported values: 5m, 1h, today, yesterday, 7d, etc."
58
+
59
+ # Prepare request payload
60
+ payload = {
61
+ "query": query,
62
+ "match_threshold": match_threshold,
63
+ "match_count": match_count,
64
+ }
65
+
66
+ # Add time filters if provided
67
+ if start_time:
68
+ payload["start_time"] = start_time.isoformat()
69
+ if end_time:
70
+ payload["end_time"] = end_time.isoformat()
71
+
72
+ # Make API request
73
+ async with httpx.AsyncClient(timeout=30.0) as client:
74
+ response = await client.post(
75
+ f"{STRAYL_API_URL}/search-logs",
76
+ json=payload,
77
+ headers={
78
+ "Authorization": f"Bearer {api_key}",
79
+ "Content-Type": "application/json",
80
+ },
81
+ )
82
+
83
+ if response.status_code != 200:
84
+ error_data = response.json() if response.headers.get("content-type") == "application/json" else {}
85
+ return f"Error: API returned status {response.status_code}: {error_data.get('error', response.text)}"
86
+
87
+ data = response.json()
88
+
89
+ if not data.get("success"):
90
+ return f"Error: {data.get('error', 'Unknown error')}"
91
+
92
+ results = data.get("results", [])
93
+ total = data.get("total_results", 0)
94
+ metadata = data.get("search_metadata", {})
95
+
96
+ if not results:
97
+ time_info = f" in period '{time_period}'" if time_period else ""
98
+ return f"No logs found for query '{query}'{time_info}"
99
+
100
+ # Format results
101
+ output = [
102
+ f"Semantic Search Results for: '{query}'",
103
+ f"Total results: {total}",
104
+ ]
105
+
106
+ if time_period:
107
+ output.append(f"Time period: {time_period}")
108
+
109
+ output.append(f"Similarity threshold: {match_threshold}")
110
+ output.append(f"Logs with embeddings: {metadata.get('logs_with_embeddings', 0)}")
111
+ output.append("\n" + "=" * 80 + "\n")
112
+
113
+ for i, log in enumerate(results[:10], 1):
114
+ output.append(f"{i}. {format_log_result(log)}")
115
+ output.append("-" * 80)
116
+
117
+ if total > 10:
118
+ output.append(f"\n... and {total - 10} more results")
119
+
120
+ return "\n".join(output)
121
+
122
+ except ValueError as e:
123
+ return f"Configuration error: {str(e)}"
124
+ except httpx.TimeoutException:
125
+ return "Error: Request timed out. Please try again."
126
+ except Exception as e:
127
+ return f"Error: {str(e)}"
128
+
129
+
130
+ @mcp.tool()
131
+ async def search_logs_exact(
132
+ query: Annotated[str, "Exact text to search for. Use '*' or empty string to see all logs"],
133
+ time_period: Annotated[Optional[str], "Time filter: 5m, 1h, today, yesterday, 7d, 30d, etc."] = None,
134
+ level: Annotated[Optional[str], "Log level filter: info, warn, error, debug"] = None,
135
+ case_sensitive: Annotated[bool, "Whether to perform case-sensitive search"] = False,
136
+ limit: Annotated[int, "Maximum number of results to return"] = 50,
137
+ ) -> str:
138
+ """Search logs using exact text matching with optional time and level filtering.
139
+
140
+ This tool performs exact text search across your logs. Use '*' as query to view all logs
141
+ with optional filters by time period and log level."""
142
+ try:
143
+ api_key = get_api_key()
144
+
145
+ # Parse time period if provided
146
+ start_time = None
147
+ end_time = None
148
+ if time_period:
149
+ start_time, end_time = parse_time_period(time_period)
150
+ if start_time is None:
151
+ return f"Error: Invalid time period '{time_period}'"
152
+
153
+ # Prepare request payload
154
+ payload = {
155
+ "query": query,
156
+ "case_sensitive": case_sensitive,
157
+ "limit": limit,
158
+ }
159
+
160
+ if level:
161
+ if level.lower() not in ["info", "warn", "error", "debug"]:
162
+ return f"Error: Invalid log level '{level}'. Must be one of: info, warn, error, debug"
163
+ payload["level"] = level.lower()
164
+
165
+ if start_time:
166
+ payload["start_time"] = start_time.isoformat()
167
+ if end_time:
168
+ payload["end_time"] = end_time.isoformat()
169
+
170
+ # Make API request to exact search endpoint
171
+ async with httpx.AsyncClient(timeout=30.0) as client:
172
+ response = await client.post(
173
+ f"{STRAYL_API_URL}/exact-search-logs",
174
+ json=payload,
175
+ headers={
176
+ "Authorization": f"Bearer {api_key}",
177
+ "Content-Type": "application/json",
178
+ },
179
+ )
180
+
181
+ if response.status_code != 200:
182
+ error_data = response.json() if response.headers.get("content-type") == "application/json" else {}
183
+ return f"Error: API returned status {response.status_code}: {error_data.get('error', response.text)}"
184
+
185
+ data = response.json()
186
+
187
+ if not data.get("success"):
188
+ return f"Error: {data.get('error', 'Unknown error')}"
189
+
190
+ results = data.get("results", [])
191
+ total = data.get("total_results", 0)
192
+
193
+ if not results:
194
+ filters = []
195
+ if time_period:
196
+ filters.append(f"period '{time_period}'")
197
+ if level:
198
+ filters.append(f"level '{level}'")
199
+ filter_str = f" with filters: {', '.join(filters)}" if filters else ""
200
+ return f"No logs found for exact text '{query}'{filter_str}"
201
+
202
+ # Format results
203
+ output = [
204
+ f"Exact Search Results for: '{query}'",
205
+ f"Total results: {total}",
206
+ ]
207
+
208
+ if time_period:
209
+ output.append(f"Time period: {time_period}")
210
+ if level:
211
+ output.append(f"Log level: {level}")
212
+
213
+ output.append(f"Case sensitive: {case_sensitive}")
214
+ output.append("\n" + "=" * 80 + "\n")
215
+
216
+ for i, log in enumerate(results[:10], 1):
217
+ output.append(f"{i}. {format_log_result(log)}")
218
+ output.append("-" * 80)
219
+
220
+ if total > 10:
221
+ output.append(f"\n... and {total - 10} more results")
222
+
223
+ return "\n".join(output)
224
+
225
+ except ValueError as e:
226
+ return f"Configuration error: {str(e)}"
227
+ except httpx.TimeoutException:
228
+ return "Error: Request timed out. Please try again."
229
+ except Exception as e:
230
+ return f"Error: {str(e)}"
231
+
232
+
233
+ @mcp.tool()
234
+ def list_time_periods() -> str:
235
+ """
236
+ List all supported time period formats for log search.
237
+
238
+ Returns:
239
+ A formatted list of all supported time period values
240
+ """
241
+ return """Supported time periods for log search:
242
+
243
+ Minutes:
244
+ - 5m, 5_minutes, 5_mins - Last 5 minutes
245
+ - 10m, 10_minutes - Last 10 minutes
246
+ - 15m, 15_minutes - Last 15 minutes
247
+ - 30m, 30_minutes - Last 30 minutes
248
+
249
+ Hours:
250
+ - 1h, 1_hour - Last 1 hour
251
+ - 2h, 2_hours - Last 2 hours
252
+ - 6h, 6_hours - Last 6 hours
253
+ - 12h, 12_hours - Last 12 hours
254
+ - 24h, last_24_hours - Last 24 hours
255
+
256
+ Days:
257
+ - today - Today from 00:00 UTC
258
+ - yesterday - Full yesterday (00:00 to 23:59)
259
+ - 7d, last_7_days - Last 7 days
260
+ - 30d, last_30_days - Last 30 days
261
+
262
+ Examples:
263
+ - search_logs_semantic("error connecting to database", "1h")
264
+ - search_logs_exact("timeout", "today", level="error")
265
+ """
@@ -0,0 +1,81 @@
1
+ """Utility functions for the Strayl MCP server."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Optional, Tuple
5
+
6
+
7
+ def parse_time_period(period: str) -> Tuple[Optional[datetime], Optional[datetime]]:
8
+ """
9
+ Parse time period strings and return start/end datetime.
10
+
11
+ Supported periods:
12
+ - "5_minutes", "5_mins", "5m" - last 5 minutes
13
+ - "1_hour", "1h" - last 1 hour
14
+ - "today" - today from 00:00
15
+ - "yesterday" - yesterday's full day
16
+ - "last_24_hours", "24h" - last 24 hours
17
+ - "last_7_days", "7d" - last 7 days
18
+ - "last_30_days", "30d" - last 30 days
19
+
20
+ Returns:
21
+ Tuple of (start_time, end_time) as datetime objects, or (None, None) if invalid
22
+ """
23
+ now = datetime.utcnow()
24
+ period = period.lower().strip()
25
+
26
+ # Minutes
27
+ if period in ["5_minutes", "5_mins", "5m"]:
28
+ return now - timedelta(minutes=5), now
29
+ elif period in ["10_minutes", "10_mins", "10m"]:
30
+ return now - timedelta(minutes=10), now
31
+ elif period in ["15_minutes", "15_mins", "15m"]:
32
+ return now - timedelta(minutes=15), now
33
+ elif period in ["30_minutes", "30_mins", "30m"]:
34
+ return now - timedelta(minutes=30), now
35
+
36
+ # Hours
37
+ elif period in ["1_hour", "1h"]:
38
+ return now - timedelta(hours=1), now
39
+ elif period in ["2_hours", "2h"]:
40
+ return now - timedelta(hours=2), now
41
+ elif period in ["6_hours", "6h"]:
42
+ return now - timedelta(hours=6), now
43
+ elif period in ["12_hours", "12h"]:
44
+ return now - timedelta(hours=12), now
45
+ elif period in ["last_24_hours", "24h"]:
46
+ return now - timedelta(hours=24), now
47
+
48
+ # Days
49
+ elif period == "today":
50
+ start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
51
+ return start_of_day, now
52
+ elif period == "yesterday":
53
+ yesterday = now - timedelta(days=1)
54
+ start_of_yesterday = yesterday.replace(hour=0, minute=0, second=0, microsecond=0)
55
+ end_of_yesterday = start_of_yesterday + timedelta(days=1) - timedelta(microseconds=1)
56
+ return start_of_yesterday, end_of_yesterday
57
+ elif period in ["last_7_days", "7d"]:
58
+ return now - timedelta(days=7), now
59
+ elif period in ["last_30_days", "30d"]:
60
+ return now - timedelta(days=30), now
61
+
62
+ # Invalid period
63
+ return None, None
64
+
65
+
66
+ def format_log_result(log: dict) -> str:
67
+ """Format a log entry for display."""
68
+ timestamp = log.get("created_at", "Unknown time")
69
+ level = log.get("level", "info").upper()
70
+ message = log.get("message", "")
71
+ context = log.get("context", {})
72
+
73
+ result = f"[{timestamp}] [{level}] {message}"
74
+
75
+ if context:
76
+ result += f"\nContext: {context}"
77
+
78
+ if "similarity" in log:
79
+ result += f"\nSimilarity: {log['similarity']:.4f}"
80
+
81
+ return result
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: strayl-mcp-server
3
+ Version: 0.1.3
4
+ Summary: MCP server for Strayl log search with semantic and exact search capabilities
5
+ Project-URL: Homepage, https://strayl.dev
6
+ Project-URL: Documentation, https://docs.strayl.dev
7
+ Project-URL: Repository, https://github.com/strayl/strayl-mcp-server
8
+ Project-URL: Issues, https://github.com/strayl/strayl-mcp-server/issues
9
+ Author-email: Strayl <support@strayl.dev>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,logging,mcp,search,strayl
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx>=0.27.0
22
+ Requires-Dist: mcp>=0.9.0
23
+ Requires-Dist: python-dateutil>=2.8.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: black>=23.0.0; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Strayl MCP Server
32
+
33
+ MCP server for semantic and exact log search powered by Strayl.
34
+
35
+ ## Features
36
+
37
+ - **Semantic Search**: AI-powered search across your logs using vector embeddings
38
+ - **Exact Text Search**: Traditional text matching with case-sensitive options
39
+ - **Time Filtering**: Search logs by time periods (5m, 1h, today, yesterday, 7d, etc.)
40
+ - **Log Level Filtering**: Filter by log levels (info, warn, error, debug)
41
+ - **Easy Integration**: Works with Claude Desktop, Cline, and other MCP clients
42
+
43
+ ## Installation
44
+
45
+ Install via pipx (recommended):
46
+
47
+ ```bash
48
+ pipx install strayl-mcp-server
49
+ ```
50
+
51
+ Or via pip:
52
+
53
+ ```bash
54
+ pip install strayl-mcp-server
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ ### Get Your API Key
60
+
61
+ 1. Visit [https://strayl.dev](https://strayl.dev)
62
+ 2. Generate an API key
63
+ 3. Copy your API key (starts with `st_`)
64
+
65
+ ### Claude Desktop Configuration
66
+
67
+ Add to your Claude Desktop config file:
68
+
69
+ **MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
70
+
71
+ **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
72
+
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "strayl": {
77
+ "command": "pipx",
78
+ "args": ["run", "--no-cache", "strayl-mcp-server"],
79
+ "env": {
80
+ "STRAYL_API_KEY": "your_api_key_here"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Cline/Other MCP Clients
88
+
89
+ Add to your MCP settings file:
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "strayl": {
95
+ "command": "pipx",
96
+ "args": ["run", "--no-cache", "strayl-mcp-server"],
97
+ "env": {
98
+ "STRAYL_API_KEY": "your_api_key_here"
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## Available Tools
106
+
107
+ ### 1. search_logs_semantic
108
+
109
+ Semantic (AI-powered) search across your logs.
110
+
111
+ **Parameters:**
112
+ - `query` (required): Search query in natural language
113
+ - `time_period` (optional): Time filter (e.g., "5m", "1h", "today", "7d")
114
+ - `match_threshold` (optional): Similarity threshold (0.0-1.0, default 0.5)
115
+ - `match_count` (optional): Max results (default 50)
116
+
117
+ **Example:**
118
+ ```
119
+ Search for database connection errors in the last hour
120
+ ```
121
+
122
+ ### 2. search_logs_exact
123
+
124
+ Exact text matching search across your logs.
125
+
126
+ **Parameters:**
127
+ - `query` (required): Exact text to search for
128
+ - `time_period` (optional): Time filter
129
+ - `level` (optional): Log level filter ("info", "warn", "error", "debug")
130
+ - `case_sensitive` (optional): Case-sensitive search (default false)
131
+ - `limit` (optional): Max results (default 50)
132
+
133
+ **Example:**
134
+ ```
135
+ Search for exact text "timeout" in error logs from today
136
+ ```
137
+
138
+ ### 3. list_time_periods
139
+
140
+ List all supported time period formats.
141
+
142
+ ## Time Period Formats
143
+
144
+ ### Minutes
145
+ - `5m`, `5_minutes`, `5_mins` - Last 5 minutes
146
+ - `10m`, `15m`, `30m` - Last 10, 15, 30 minutes
147
+
148
+ ### Hours
149
+ - `1h`, `2h`, `6h`, `12h` - Last 1, 2, 6, 12 hours
150
+ - `24h`, `last_24_hours` - Last 24 hours
151
+
152
+ ### Days
153
+ - `today` - Today from 00:00 UTC
154
+ - `yesterday` - Full yesterday
155
+ - `7d`, `last_7_days` - Last 7 days
156
+ - `30d`, `last_30_days` - Last 30 days
157
+
158
+ ## Usage Examples
159
+
160
+ ### With Claude Desktop
161
+
162
+ Simply ask Claude:
163
+
164
+ > "Search my logs for authentication errors in the last hour"
165
+
166
+ > "Find all database connection issues from today"
167
+
168
+ > "Show me exact text 'null pointer' in error logs"
169
+
170
+ ### Development/Testing
171
+
172
+ Run the server directly:
173
+
174
+ ```bash
175
+ export STRAYL_API_KEY="your_api_key_here"
176
+ strayl-mcp-server
177
+ ```
178
+
179
+ ## Logging Your Application
180
+
181
+ To send logs to Strayl, use the Strayl Log API:
182
+
183
+ ```python
184
+ import httpx
185
+
186
+ api_key = "st_your_api_key"
187
+ api_url = "https://ougtygyvcgdnytkswier.supabase.co/functions/v1"
188
+
189
+ async def log_message(message: str, level: str = "info", context: dict = None):
190
+ async with httpx.AsyncClient() as client:
191
+ await client.post(
192
+ f"{api_url}/log",
193
+ json={
194
+ "message": message,
195
+ "level": level,
196
+ "context": context or {}
197
+ },
198
+ headers={"Authorization": f"Bearer {api_key}"}
199
+ )
200
+
201
+ # Usage
202
+ await log_message("User logged in", "info", {"user_id": "123"})
203
+ await log_message("Database connection failed", "error", {"db": "postgres"})
204
+ ```
205
+
206
+ ## Troubleshooting
207
+
208
+ ### API Key Issues
209
+
210
+ If you get authentication errors:
211
+ 1. Verify your API key starts with `st_`
212
+ 2. Check the API key is correctly set in your MCP config
213
+ 3. Ensure there are no extra spaces or quotes around the key
214
+
215
+ ### Connection Issues
216
+
217
+ If the server fails to connect:
218
+ 1. Check your internet connection
219
+ 2. Verify the Strayl API is accessible
220
+ 3. Check for any firewall or proxy issues
221
+
222
+ ### No Results
223
+
224
+ If searches return no results:
225
+ 1. Verify you've sent logs to Strayl
226
+ 2. Check the time period filter isn't too restrictive
227
+ 3. For semantic search, wait a few seconds for embeddings to generate
228
+
229
+ ## Support
230
+
231
+ - Documentation: [https://docs.strayl.dev](https://docs.strayl.dev)
232
+ - Issues: [https://github.com/strayl/strayl-mcp-server/issues](https://github.com/strayl/strayl-mcp-server/issues)
233
+ - Website: [https://strayl.dev](https://strayl.dev)
234
+
235
+ ## License
236
+
237
+ MIT License
@@ -0,0 +1,9 @@
1
+ strayl_mcp_server/__init__.py,sha256=YlZZgEDWLnSP-LyCqePauNh5Az83gQwr8r-f3p2Qvt8,76
2
+ strayl_mcp_server/__main__.py,sha256=NjGxwuqovfeHDFoPaZ1Q7n_B21rufsCKs0BYR1S2GfU,175
3
+ strayl_mcp_server/server.py,sha256=R_DI3jaP1DMKaMGaSY9ZWZK_n0gYDzB0DlOCKqsZ1iU,9136
4
+ strayl_mcp_server/utils.py,sha256=3AnU2_xbgY4V4qYZrz5oeFlVillfuL7_y6HHmp2eSV0,2778
5
+ strayl_mcp_server-0.1.3.dist-info/METADATA,sha256=LkWCc7BqgLscqOW6l0OPXAwplipdzIBNmr_WP0P8qPU,6045
6
+ strayl_mcp_server-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ strayl_mcp_server-0.1.3.dist-info/entry_points.txt,sha256=vWFdsH9LlqPHhksTI_cXM1s23TPayuAsAimjHmikhZI,70
8
+ strayl_mcp_server-0.1.3.dist-info/licenses/LICENSE,sha256=tFZq-Op9ait9HfpxRqREcfdp3NoUZAX2PalftrBG_3c,1063
9
+ strayl_mcp_server-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ strayl-mcp-server = strayl_mcp_server.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Strayl
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.