marble-headed-mcp 0.1.21 → 0.1.24
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/README.md +1 -0
- package/dist/index.js +164 -4
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# marble-headed-mcp
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
const DEFAULT_BASE_URL = 'http://localhost:4000';
|
|
10
|
+
const WEBAPP_LOG_PATH = '/tmp/webapp';
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
function normalizeBaseUrl(input) {
|
|
12
13
|
return input.replace(/\/+$/, '');
|
|
@@ -79,6 +80,129 @@ async function getJson(pathname) {
|
|
|
79
80
|
}
|
|
80
81
|
return { status: response.status, ok: response.ok, text, json };
|
|
81
82
|
}
|
|
83
|
+
function splitLogLines(content) {
|
|
84
|
+
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
85
|
+
const lines = normalized.split('\n');
|
|
86
|
+
if (lines.length && lines[lines.length - 1] === '') {
|
|
87
|
+
lines.pop();
|
|
88
|
+
}
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
async function readWebappLogLines() {
|
|
92
|
+
try {
|
|
93
|
+
const content = await fs.readFile(WEBAPP_LOG_PATH, 'utf8');
|
|
94
|
+
return { ok: true, lines: splitLogLines(content) };
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return { ok: false, error: error?.message || String(error) };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function normalizePositiveInt(value) {
|
|
101
|
+
if (typeof value !== 'number' || Number.isNaN(value))
|
|
102
|
+
return null;
|
|
103
|
+
const rounded = Math.floor(value);
|
|
104
|
+
if (rounded <= 0)
|
|
105
|
+
return null;
|
|
106
|
+
return rounded;
|
|
107
|
+
}
|
|
108
|
+
function buildLineRecords(lines, startIndex) {
|
|
109
|
+
const records = [];
|
|
110
|
+
for (let i = startIndex; i < lines.length; i += 1) {
|
|
111
|
+
records.push({ lineNumber: i + 1, text: lines[i] });
|
|
112
|
+
}
|
|
113
|
+
return records;
|
|
114
|
+
}
|
|
115
|
+
function buildMatcher(pattern) {
|
|
116
|
+
const trimmed = typeof pattern === 'string' ? pattern.trim() : '';
|
|
117
|
+
if (!trimmed) {
|
|
118
|
+
return () => true;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const regex = new RegExp(trimmed);
|
|
122
|
+
return (line) => regex.test(line);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return (line) => line.includes(trimmed);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function mergeRanges(ranges) {
|
|
129
|
+
if (!ranges.length)
|
|
130
|
+
return [];
|
|
131
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
132
|
+
const merged = [];
|
|
133
|
+
for (const range of sorted) {
|
|
134
|
+
const last = merged[merged.length - 1];
|
|
135
|
+
if (!last || range.start > last.end + 1) {
|
|
136
|
+
merged.push({ start: range.start, end: range.end });
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
last.end = Math.max(last.end, range.end);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return merged;
|
|
143
|
+
}
|
|
144
|
+
async function fetchAllWebappLogs(args) {
|
|
145
|
+
const numberOfHits = normalizePositiveInt(args.number_of_hits);
|
|
146
|
+
if (!numberOfHits) {
|
|
147
|
+
return { ok: false, error: 'number_of_hits must be a positive integer.' };
|
|
148
|
+
}
|
|
149
|
+
const readResult = await readWebappLogLines();
|
|
150
|
+
if (!readResult.ok) {
|
|
151
|
+
return { ok: false, error: readResult.error };
|
|
152
|
+
}
|
|
153
|
+
const totalLines = readResult.lines.length;
|
|
154
|
+
const startIndex = Math.max(0, totalLines - numberOfHits);
|
|
155
|
+
const selected = readResult.lines.slice(startIndex);
|
|
156
|
+
const lines = buildLineRecords(selected, startIndex);
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
totalLines,
|
|
160
|
+
returnedLines: lines.length,
|
|
161
|
+
lines,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function fetchGrepWebappLogs(args) {
|
|
165
|
+
const numberOfHits = normalizePositiveInt(args.number_of_hits);
|
|
166
|
+
if (!numberOfHits) {
|
|
167
|
+
return { ok: false, error: 'number_of_hits must be a positive integer.' };
|
|
168
|
+
}
|
|
169
|
+
const context = normalizePositiveInt(args.context) ?? 0;
|
|
170
|
+
const matcher = buildMatcher(args.grep_pattern);
|
|
171
|
+
const readResult = await readWebappLogLines();
|
|
172
|
+
if (!readResult.ok) {
|
|
173
|
+
return { ok: false, error: readResult.error };
|
|
174
|
+
}
|
|
175
|
+
const lines = readResult.lines;
|
|
176
|
+
const hitIndices = [];
|
|
177
|
+
const hitLines = [];
|
|
178
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
179
|
+
if (matcher(lines[index])) {
|
|
180
|
+
hitIndices.push(index);
|
|
181
|
+
hitLines.push({ lineNumber: index + 1, text: lines[index] });
|
|
182
|
+
if (hitIndices.length >= numberOfHits) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const ranges = mergeRanges(hitIndices.map((hit) => ({
|
|
188
|
+
start: Math.max(0, hit - context),
|
|
189
|
+
end: Math.min(lines.length - 1, hit + context),
|
|
190
|
+
})));
|
|
191
|
+
const contextLines = [];
|
|
192
|
+
for (const range of ranges) {
|
|
193
|
+
for (let i = range.start; i <= range.end; i += 1) {
|
|
194
|
+
contextLines.push({ lineNumber: i + 1, text: lines[i] });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
ok: true,
|
|
199
|
+
totalLines: lines.length,
|
|
200
|
+
matchedHits: hitIndices.length,
|
|
201
|
+
hitLines,
|
|
202
|
+
contextLines,
|
|
203
|
+
ranges: ranges.map((range) => ({ startLine: range.start + 1, endLine: range.end + 1 })),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
82
206
|
async function fetchWorkflowEntries(runId) {
|
|
83
207
|
if (!runId)
|
|
84
208
|
return null;
|
|
@@ -314,9 +438,11 @@ const TOOLS = [
|
|
|
314
438
|
type: 'object',
|
|
315
439
|
properties: {
|
|
316
440
|
projectId: { type: 'number', description: 'Project id to fetch logs for.' },
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
441
|
+
timestampStart: { type: 'string', description: 'Start timestamp (ISO). Optional. Legacy `timeStart` is also accepted.' },
|
|
442
|
+
timestampEnd: { type: 'string', description: 'End timestamp (ISO). Optional. Legacy `timeEnd` is also accepted.' },
|
|
443
|
+
timeStart: { type: 'string', description: 'Legacy start timestamp (ISO). Optional.' },
|
|
444
|
+
timeEnd: { type: 'string', description: 'Legacy end timestamp (ISO). Optional.' },
|
|
445
|
+
freshness: { type: 'string', description: 'Relative freshness (e.g., "3h" or "1d") to fetch recent logs instead of timestamps.' },
|
|
320
446
|
},
|
|
321
447
|
required: ['projectId'],
|
|
322
448
|
additionalProperties: false,
|
|
@@ -334,6 +460,32 @@ const TOOLS = [
|
|
|
334
460
|
additionalProperties: false,
|
|
335
461
|
},
|
|
336
462
|
},
|
|
463
|
+
{
|
|
464
|
+
name: 'fetch_all_webapp_logs',
|
|
465
|
+
description: 'Get the last number_of_hits logs from /tmp/webapp.',
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: 'object',
|
|
468
|
+
properties: {
|
|
469
|
+
number_of_hits: { type: 'number', description: 'Number of log lines to return from the end of /tmp/webapp.' },
|
|
470
|
+
},
|
|
471
|
+
required: ['number_of_hits'],
|
|
472
|
+
additionalProperties: false,
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'fetch_grep_webapp_logs',
|
|
477
|
+
description: 'Grep backwards from the end of /tmp/webapp for number_of_hits matches and return context lines without duplication.',
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: 'object',
|
|
480
|
+
properties: {
|
|
481
|
+
number_of_hits: { type: 'number', description: 'Number of matching log hits to return from the end of /tmp/webapp.' },
|
|
482
|
+
grep_pattern: { type: 'string', description: 'Optional regex pattern to match log lines.' },
|
|
483
|
+
context: { type: 'number', description: 'Number of surrounding lines to include around each hit.' },
|
|
484
|
+
},
|
|
485
|
+
required: ['number_of_hits'],
|
|
486
|
+
additionalProperties: false,
|
|
487
|
+
},
|
|
488
|
+
},
|
|
337
489
|
{
|
|
338
490
|
name: 'save_image_url',
|
|
339
491
|
description: 'Download an image URL to /tmp and return the file path.',
|
|
@@ -462,7 +614,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
462
614
|
return { content: [{ type: 'text', text: JSON.stringify(payload || { status: result.status, body: result.text }, null, 2) }] };
|
|
463
615
|
}
|
|
464
616
|
case 'get_container_logs': {
|
|
465
|
-
const result = await
|
|
617
|
+
const result = await postHeadedJson('/get_container_logs', args);
|
|
466
618
|
const payload = (result.json && typeof result.json === 'object') ? result.json : null;
|
|
467
619
|
if (!result.ok || !payload) {
|
|
468
620
|
const fallbackText = result.text || '';
|
|
@@ -521,6 +673,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
521
673
|
const result = await postHeadedJson('/get_instance_files', args);
|
|
522
674
|
return { content: [{ type: 'text', text: JSON.stringify(result.json || { status: result.status, body: result.text }, null, 2) }] };
|
|
523
675
|
}
|
|
676
|
+
case 'fetch_all_webapp_logs': {
|
|
677
|
+
const result = await fetchAllWebappLogs(args);
|
|
678
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
679
|
+
}
|
|
680
|
+
case 'fetch_grep_webapp_logs': {
|
|
681
|
+
const result = await fetchGrepWebappLogs(args);
|
|
682
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
683
|
+
}
|
|
524
684
|
case 'save_image_url': {
|
|
525
685
|
const saveResult = await saveImageFromUrl({
|
|
526
686
|
url: args?.url,
|