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.
- strayl_mcp_server/__init__.py +3 -0
- strayl_mcp_server/__main__.py +12 -0
- strayl_mcp_server/server.py +265 -0
- strayl_mcp_server/utils.py +81 -0
- strayl_mcp_server-0.1.3.dist-info/METADATA +237 -0
- strayl_mcp_server-0.1.3.dist-info/RECORD +9 -0
- strayl_mcp_server-0.1.3.dist-info/WHEEL +4 -0
- strayl_mcp_server-0.1.3.dist-info/entry_points.txt +2 -0
- strayl_mcp_server-0.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -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,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.
|