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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +164 -4
  3. 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
- timeStart: { type: 'string', description: 'Start timestamp (ISO). Optional.' },
318
- timeEnd: { type: 'string', description: 'End timestamp (ISO). Optional.' },
319
- limit: { type: 'number', description: 'Maximum number of log entries to return. A reasonable default is 1000.' },
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 postJson('/api/gcloud/read', args);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marble-headed-mcp",
3
- "version": "0.1.21",
3
+ "version": "0.1.24",
4
4
  "description": "MCP server for Marble headed automation endpoints",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",