You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CHANGELOG.md
+7-1Lines changed: 7 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# Changelog
2
2
3
-
## [v1.1.0] - 2026-08-16
3
+
## [v1.1.0] - 2026-08-18
4
4
5
5
**MCP — the AI log access layer.** tailr is now both a human log viewer and an MCP server: AI agents (Claude Code, Cursor, Codex, OpenCode, any MCP client) can list, search, count, and read server logs directly over `/mcp` — no SSH, no copy-paste. Single binary, zero new runtime dependencies.
6
6
@@ -21,6 +21,12 @@
21
21
-**`tail_log` byte cap no longer truncates silently** — responses carry `truncated: true` when the 64MB read cap is hit.
22
22
-**Token comparison is constant-time** (hand-rolled, zero new dependencies) for both the Bearer header and the WS query-param fallback.
23
23
-**Disabled `/mcp` returns a deterministic 404** instead of falling into the SPA fallback (which 500s when no frontend dist is built).
24
+
-**`tail_log` dropped empty lines and shifted line numbers** (external review) — the tail reader filtered all empty split segments, so files with blank lines got line numbers drifting from the scanner's. Empty lines now occupy their real line numbers; protocol-level regression test added.
25
+
-**`tail_log` could report wrong line numbers from a partial line count** (external review) — if the line-count scan hit its time budget, the partial total was used as exact. Responses now carry `incomplete: true` in that case and the tool description tells agents to treat line numbers as approximate.
26
+
-**Stats caching (mtime + size keyed)** — completed line/level counts are cached; repeated `tail_log`/`get_log_stats` calls on append-only logs no longer rescan the whole file. Cache hits skip the scan semaphore; file growth invalidates.
27
+
-**Startup warning when MCP is enabled without a token** — the endpoint adds unauthenticated expensive-scan capability on top of the existing no-auth REST default; the combination is now logged loudly.
28
+
-**LevelDetector no longer panics on keywords longer than the 256-byte scan window** (external review) — `limit - len` underflowed (debug panic / release slice OOB). Oversized keywords are now skipped safely.
29
+
-**`list_log_files` signals truncation** — hitting the 5000-entry cap now returns `truncated: true` instead of silently dropping files; `search_logs` description clarifies that multi-page `count_only` results must be summed across pages.
#[tool(description = "List the log files accessible on this server. Returns name/path/size/isDir for each entry. Start here to discover what you can query — the `path` values are the exact identifiers the other tools expect.")]
#[tool(description = "Read the LAST N lines of a log file (default 100, max 5000) with absolute line numbers. Good for a quick look at recent activity; prefer get_log_stats + search_logs for anything bigger. If truncated is true, the tail exceeded the byte cap — request fewer lines.")]
268
+
#[tool(description = "Read the LAST N lines of a log file (default 100, max 5000) with absolute line numbers. Good for a quick look at recent activity; prefer get_log_stats + search_logs for anything bigger. If truncated is true, the tail exceeded the byte cap — request fewer lines. If incomplete is true, line numbers are approximate (the line count did not finish scanning).")]
267
269
asyncfntail_log(
268
270
&self,
269
271
Parameters(args):Parameters<TailLogArgs>,
@@ -305,10 +307,16 @@ impl McpTools {
305
307
.map_err(query_io)?;
306
308
let file_len = std::fs::metadata(&path_buf).map(|m| m.len()).unwrap_or(0);
307
309
let tail_truncated = file_len.saturating_sub(tail.start_byte) > read asu64;
310
+
// 只丢弃末尾换行产生的空元素;中间的空行是真实行,
311
+
// 必须保留并占一个行号——否则与 scanner 的行号体系错位
312
+
// (空行后所有行号整体漂移)。
313
+
letmut parts:Vec<&[u8]> = buf.split(|&b| b == b'\n').collect();
314
+
if parts.last() == Some(&&[][..]){
315
+
parts.pop();
316
+
}
308
317
letmut line_no = start_line;
309
-
let lines:Vec<serde_json::Value> = buf
310
-
.split(|&b| b == b'\n')
311
-
.filter(|l| !l.is_empty())
318
+
let lines:Vec<serde_json::Value> = parts
319
+
.into_iter()
312
320
.map(|l| {
313
321
letmut end = l.len();
314
322
if end > 0 && l[end - 1] == b'\r'{
@@ -325,6 +333,9 @@ impl McpTools {
325
333
"totalLines": total_lines,
326
334
"sizeBytes": stats.total_bytes,
327
335
"truncated": tail_truncated,
336
+
// 行数统计未扫完时 start_line/line 是按部分计数推的,
337
+
// 只能当近似值——显式透传,别让 AI 当精确数用。
338
+
"incomplete": !stats.completed,
328
339
}))
329
340
})
330
341
.await
@@ -341,7 +352,7 @@ impl McpTools {
341
352
)]))
342
353
}
343
354
344
-
#[tool(description = "Search a log file for lines containing ALL keywords (AND, case-insensitive). For 'how many/how often' questions set count_only=true (fast, counts without content). For inspection, returns matching lines merged into context windows (± context_lines). Workflow: get_log_stats first, then search with 2-3 specific keywords. If more/truncated is true, continue with resumeCursor (never restart); a timeout also returns partial results + resumeCursor — just continue. When reporting results, state the raw matched LINE count first, then any higher-level grouping you derive (e.g. request-pairs) — users usually think in lines.")]
355
+
#[tool(description = "Search a log file for lines containing ALL keywords (AND, case-insensitive). For 'how many/how often' questions set count_only=true (fast, counts without content). For inspection, returns matching lines merged into context windows (± context_lines). Workflow: get_log_stats first, then search with 2-3 specific keywords. If more/truncated is true, continue with resumeCursor (never restart); a timeout also returns partial results + resumeCursor — just continue. When count_only spans several pages, sum matchedLines across all pages for the total. When reporting results, state the raw matched LINE count first, then any higher-level grouping you derive (e.g. request-pairs) — users usually think in lines.")]
345
356
asyncfnsearch_logs(
346
357
&self,
347
358
Parameters(args):Parameters<SearchLogsArgs>,
@@ -437,7 +448,9 @@ impl ServerHandler for McpTools {
0 commit comments