cubeapm-mcp 1.3.0 → 1.3.1
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.
- package/dist/index.js +88 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25,6 +25,90 @@ const server = new McpServer({
|
|
|
25
25
|
// ============================================
|
|
26
26
|
// LOGS APIs
|
|
27
27
|
// ============================================
|
|
28
|
+
// Convert RFC3339 string or Unix timestamp (s/ms/μs/ns) to nanoseconds.
|
|
29
|
+
// CubeAPM's prod UI embeds time in queries as `_time:[ns_start, ns_end)`,
|
|
30
|
+
// so we mirror that wire format rather than relying on start/end form params.
|
|
31
|
+
function toNanoseconds(t) {
|
|
32
|
+
if (/^\d+$/.test(t)) {
|
|
33
|
+
const n = BigInt(t);
|
|
34
|
+
if (n < 10000000000n)
|
|
35
|
+
return (n * 1000000000n).toString(); // seconds
|
|
36
|
+
if (n < 10000000000000n)
|
|
37
|
+
return (n * 1000000n).toString(); // milliseconds
|
|
38
|
+
if (n < 10000000000000000n)
|
|
39
|
+
return (n * 1000n).toString(); // microseconds
|
|
40
|
+
return n.toString(); // nanoseconds
|
|
41
|
+
}
|
|
42
|
+
const ms = new Date(t).getTime();
|
|
43
|
+
if (Number.isNaN(ms))
|
|
44
|
+
throw new Error(`Invalid timestamp: ${t}`);
|
|
45
|
+
return (BigInt(ms) * 1000000n).toString();
|
|
46
|
+
}
|
|
47
|
+
// Find the index of the first top-level pipe operator in a LogsQL query,
|
|
48
|
+
// ignoring `|` inside quoted strings (regex alternation like `=~"a|b"`),
|
|
49
|
+
// brace groups `{...}`, or parens `(...)`. Returns -1 if no pipe found.
|
|
50
|
+
function findFirstTopLevelPipe(query) {
|
|
51
|
+
let inDouble = false;
|
|
52
|
+
let inSingle = false;
|
|
53
|
+
let braceDepth = 0;
|
|
54
|
+
let parenDepth = 0;
|
|
55
|
+
for (let i = 0; i < query.length; i++) {
|
|
56
|
+
const c = query[i];
|
|
57
|
+
const prev = i > 0 ? query[i - 1] : "";
|
|
58
|
+
// Honour backslash escapes inside strings (e.g. `"shopify\\-prod"`).
|
|
59
|
+
if (prev === "\\")
|
|
60
|
+
continue;
|
|
61
|
+
if (inDouble) {
|
|
62
|
+
if (c === '"')
|
|
63
|
+
inDouble = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (inSingle) {
|
|
67
|
+
if (c === "'")
|
|
68
|
+
inSingle = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === '"') {
|
|
72
|
+
inDouble = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === "'") {
|
|
76
|
+
inSingle = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (c === "{") {
|
|
80
|
+
braceDepth++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === "}") {
|
|
84
|
+
braceDepth--;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (c === "(") {
|
|
88
|
+
parenDepth++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (c === ")") {
|
|
92
|
+
parenDepth--;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (c === "|" && braceDepth === 0 && parenDepth === 0)
|
|
96
|
+
return i;
|
|
97
|
+
}
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
// Inject `_time:[start_ns,end_ns)` before the first top-level pipe operator
|
|
101
|
+
// (or at end if none). Quote/brace/paren aware to handle regex alternations
|
|
102
|
+
// like `=~"shopify-latest|shopify-prod"` without splitting mid-string.
|
|
103
|
+
function injectTimeFilter(query, startNs, endNs) {
|
|
104
|
+
const timeFilter = `_time:[${startNs},${endNs})`;
|
|
105
|
+
const pipeIdx = findFirstTopLevelPipe(query);
|
|
106
|
+
if (pipeIdx === -1)
|
|
107
|
+
return `${query.trim()} ${timeFilter}`.trim();
|
|
108
|
+
const head = query.slice(0, pipeIdx).trim();
|
|
109
|
+
const tail = query.slice(pipeIdx);
|
|
110
|
+
return `${head} ${timeFilter} ${tail}`;
|
|
111
|
+
}
|
|
28
112
|
server.tool("query_logs", `Query logs from CubeAPM using LogsQL syntax (VictoriaLogs compatible). Returns log entries matching the query.
|
|
29
113
|
|
|
30
114
|
IMPORTANT - Log Label Discovery:
|
|
@@ -79,10 +163,11 @@ Example queries:
|
|
|
79
163
|
end: z.string().describe("End time in RFC3339 format or Unix timestamp in seconds"),
|
|
80
164
|
limit: z.number().optional().default(100).describe("Maximum log entries to return (default: 100)"),
|
|
81
165
|
}, async ({ query, start, end, limit }) => {
|
|
166
|
+
const startNs = toNanoseconds(start);
|
|
167
|
+
const endNs = toNanoseconds(end);
|
|
168
|
+
const finalQuery = injectTimeFilter(query, startNs, endNs);
|
|
82
169
|
const params = new URLSearchParams({
|
|
83
|
-
query,
|
|
84
|
-
start,
|
|
85
|
-
end,
|
|
170
|
+
query: finalQuery,
|
|
86
171
|
limit: String(limit),
|
|
87
172
|
});
|
|
88
173
|
const response = await fetch(`${queryBaseUrl}/api/logs/select/logsql/query`, {
|
package/package.json
CHANGED