repack-logs-mcp 1.0.0

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.
@@ -0,0 +1,164 @@
1
+ import { watch } from 'chokidar';
2
+ import { readFile, stat } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ /**
5
+ * Watches a Re.Pack log file and streams entries to a LogStore.
6
+ */
7
+ export class LogWatcher {
8
+ filePath;
9
+ store;
10
+ watcher = null;
11
+ lastPosition = 0;
12
+ watching = false;
13
+ constructor(filePath, store) {
14
+ this.filePath = filePath;
15
+ this.store = store;
16
+ }
17
+ /**
18
+ * Start watching the log file.
19
+ */
20
+ async start() {
21
+ if (this.watching)
22
+ return;
23
+ // Read existing content first
24
+ await this.readNewContent();
25
+ // Watch for changes
26
+ this.watcher = watch(this.filePath, {
27
+ persistent: true,
28
+ ignoreInitial: true,
29
+ awaitWriteFinish: {
30
+ stabilityThreshold: 100,
31
+ pollInterval: 50,
32
+ },
33
+ });
34
+ this.watcher.on('change', async () => {
35
+ await this.readNewContent();
36
+ });
37
+ this.watcher.on('add', async () => {
38
+ // File was created, reset position and read
39
+ this.lastPosition = 0;
40
+ await this.readNewContent();
41
+ });
42
+ this.watcher.on('unlink', () => {
43
+ // File was deleted, reset position
44
+ this.lastPosition = 0;
45
+ });
46
+ this.watching = true;
47
+ }
48
+ /**
49
+ * Stop watching the log file.
50
+ */
51
+ async stop() {
52
+ if (this.watcher) {
53
+ await this.watcher.close();
54
+ this.watcher = null;
55
+ }
56
+ this.watching = false;
57
+ }
58
+ /**
59
+ * Read new content from the log file since last position.
60
+ */
61
+ async readNewContent() {
62
+ if (!existsSync(this.filePath)) {
63
+ return;
64
+ }
65
+ try {
66
+ const stats = await stat(this.filePath);
67
+ // If file was truncated, reset position
68
+ if (stats.size < this.lastPosition) {
69
+ this.lastPosition = 0;
70
+ }
71
+ // Read only new content
72
+ const content = await readFile(this.filePath, 'utf-8');
73
+ const newContent = content.slice(this.lastPosition);
74
+ if (newContent.length === 0)
75
+ return;
76
+ // Update position
77
+ this.lastPosition = content.length;
78
+ // Parse JSON lines
79
+ const lines = newContent.split('\n').filter(line => line.trim());
80
+ for (const line of lines) {
81
+ try {
82
+ const entry = this.parseLine(line);
83
+ if (entry) {
84
+ this.store.add(entry);
85
+ }
86
+ }
87
+ catch {
88
+ // Skip malformed lines
89
+ }
90
+ }
91
+ }
92
+ catch {
93
+ // File might not exist yet or be inaccessible
94
+ }
95
+ }
96
+ /**
97
+ * Parse a single log line into a LogEntry.
98
+ */
99
+ parseLine(line) {
100
+ try {
101
+ const parsed = JSON.parse(line);
102
+ // Ensure required fields exist
103
+ if (!parsed.message && !parsed.msg) {
104
+ return null;
105
+ }
106
+ return {
107
+ timestamp: parsed.timestamp ?? parsed.time ?? new Date().toISOString(),
108
+ type: this.normalizeType(parsed.type ?? parsed.level ?? 'info'),
109
+ message: parsed.message ?? parsed.msg ?? '',
110
+ issuer: parsed.issuer ?? parsed.source ?? parsed.name,
111
+ request: parsed.request,
112
+ file: parsed.file ?? parsed.filename,
113
+ loader: parsed.loader,
114
+ stack: parsed.stack,
115
+ duration: parsed.duration,
116
+ ...parsed,
117
+ };
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ /**
124
+ * Normalize various log level formats to our LogType.
125
+ */
126
+ normalizeType(type) {
127
+ const normalized = type.toLowerCase();
128
+ if (normalized.includes('error') || normalized === 'err') {
129
+ return 'error';
130
+ }
131
+ if (normalized.includes('warn')) {
132
+ return 'warn';
133
+ }
134
+ if (normalized.includes('debug') || normalized === 'trace') {
135
+ return 'debug';
136
+ }
137
+ if (normalized.includes('success') || normalized === 'done') {
138
+ return 'success';
139
+ }
140
+ if (normalized.includes('progress')) {
141
+ return 'progress';
142
+ }
143
+ return 'info';
144
+ }
145
+ /**
146
+ * Check if the watcher is currently active.
147
+ */
148
+ get isWatching() {
149
+ return this.watching;
150
+ }
151
+ /**
152
+ * Check if the log file exists.
153
+ */
154
+ get fileExists() {
155
+ return existsSync(this.filePath);
156
+ }
157
+ /**
158
+ * Get the path being watched.
159
+ */
160
+ get path() {
161
+ return this.filePath;
162
+ }
163
+ }
164
+ //# sourceMappingURL=log-watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-watcher.js","sourceRoot":"","sources":["../src/log-watcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Log entry structure from Re.Pack's JSON log output.
3
+ * Re.Pack outputs JSON lines with webpack compilation info.
4
+ */
5
+ export interface LogEntry {
6
+ timestamp: string;
7
+ type: LogType;
8
+ issuer?: string;
9
+ message: string;
10
+ /** Module request path */
11
+ request?: string;
12
+ /** File path being processed */
13
+ file?: string;
14
+ /** Webpack loader info */
15
+ loader?: string;
16
+ /** Error stack trace */
17
+ stack?: string;
18
+ /** Build duration in ms */
19
+ duration?: number;
20
+ /** Additional metadata */
21
+ [key: string]: unknown;
22
+ }
23
+ export type LogType = 'info' | 'warn' | 'error' | 'debug' | 'success' | 'progress';
24
+ export interface LogFilter {
25
+ /** Filter by log type(s) */
26
+ types?: LogType[];
27
+ /** Maximum number of logs to return */
28
+ limit?: number;
29
+ /** Only logs after this timestamp (ISO string) */
30
+ since?: string;
31
+ /** Filter by issuer/source */
32
+ issuer?: string;
33
+ /** Search in message content */
34
+ search?: string;
35
+ }
36
+ export interface WatcherStatus {
37
+ watching: boolean;
38
+ filePath: string;
39
+ fileExists: boolean;
40
+ logCount: number;
41
+ errorCount: number;
42
+ warningCount: number;
43
+ lastUpdate: string | null;
44
+ }
45
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,OAAO,GACf,MAAM,GACN,MAAM,GACN,OAAO,GACP,OAAO,GACP,SAAS,GACT,UAAU,CAAC;AAEf,MAAM,WAAW,SAAS;IACxB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "repack-logs-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for tailing Re.Pack/Rock.js dev server logs",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "repack-logs-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "repack",
18
+ "react-native",
19
+ "logs",
20
+ "dev-tools"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.0.0",
26
+ "chokidar": "^4.0.0",
27
+ "zod": "^3.23.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "typescript": "^5.6.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ }
36
+ }
package/src/config.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { resolve } from 'path';
2
+
3
+ export interface Config {
4
+ logFilePath: string;
5
+ maxLogs: number;
6
+ }
7
+
8
+ const DEFAULT_LOG_FILE = '.repack-logs.json';
9
+ const DEFAULT_MAX_LOGS = 1000;
10
+
11
+ /**
12
+ * Parse configuration from CLI args and environment variables.
13
+ *
14
+ * Priority:
15
+ * 1. CLI argument (first positional arg)
16
+ * 2. REPACK_LOG_FILE environment variable
17
+ * 3. Default: .repack-logs.json in current directory
18
+ */
19
+ export function getConfig(): Config {
20
+ const args = process.argv.slice(2);
21
+
22
+ // First positional argument is the log file path
23
+ const cliPath = args.find(arg => !arg.startsWith('-'));
24
+
25
+ const logFilePath = cliPath
26
+ ?? process.env.REPACK_LOG_FILE
27
+ ?? DEFAULT_LOG_FILE;
28
+
29
+ const maxLogs = process.env.REPACK_MAX_LOGS
30
+ ? parseInt(process.env.REPACK_MAX_LOGS, 10)
31
+ : DEFAULT_MAX_LOGS;
32
+
33
+ return {
34
+ logFilePath: resolve(logFilePath),
35
+ maxLogs,
36
+ };
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+ import { getConfig } from './config.js';
7
+ import { LogStore } from './log-store.js';
8
+ import { LogWatcher } from './log-watcher.js';
9
+ import type { LogType } from './types.js';
10
+
11
+ const config = getConfig();
12
+ const store = new LogStore(config.maxLogs);
13
+ const watcher = new LogWatcher(config.logFilePath, store);
14
+
15
+ // Create MCP server
16
+ const server = new McpServer({
17
+ name: 'repack-logs-mcp',
18
+ version: '1.0.0',
19
+ });
20
+
21
+ // Tool: get_build_logs
22
+ server.tool(
23
+ 'get_build_logs',
24
+ 'Get recent Re.Pack build logs with optional filtering',
25
+ {
26
+ limit: z.number().optional().describe('Maximum number of logs to return (default: 50)'),
27
+ types: z.array(z.enum(['info', 'warn', 'error', 'debug', 'success', 'progress']))
28
+ .optional()
29
+ .describe('Filter by log type(s)'),
30
+ since: z.string().optional().describe('Only logs after this ISO timestamp'),
31
+ issuer: z.string().optional().describe('Filter by issuer/source name'),
32
+ search: z.string().optional().describe('Search in log messages'),
33
+ },
34
+ async (args) => {
35
+ const logs = store.get({
36
+ limit: args.limit ?? 50,
37
+ types: args.types as LogType[] | undefined,
38
+ since: args.since,
39
+ issuer: args.issuer,
40
+ search: args.search,
41
+ });
42
+
43
+ if (logs.length === 0) {
44
+ return {
45
+ content: [{
46
+ type: 'text',
47
+ text: 'No logs found matching the criteria.',
48
+ }],
49
+ };
50
+ }
51
+
52
+ const formatted = logs.map(log => {
53
+ const parts = [
54
+ `[${log.timestamp}]`,
55
+ `[${log.type.toUpperCase()}]`,
56
+ ];
57
+ if (log.issuer) parts.push(`[${log.issuer}]`);
58
+ parts.push(log.message);
59
+ if (log.file) parts.push(`\n File: ${log.file}`);
60
+ if (log.stack) parts.push(`\n Stack: ${log.stack}`);
61
+ return parts.join(' ');
62
+ }).join('\n\n');
63
+
64
+ return {
65
+ content: [{
66
+ type: 'text',
67
+ text: `Found ${logs.length} log(s):\n\n${formatted}`,
68
+ }],
69
+ };
70
+ }
71
+ );
72
+
73
+ // Tool: get_errors
74
+ server.tool(
75
+ 'get_errors',
76
+ 'Get only errors and warnings from Re.Pack build logs',
77
+ {
78
+ limit: z.number().optional().describe('Maximum number of errors to return (default: 20)'),
79
+ },
80
+ async (args) => {
81
+ const errors = store.getErrors(args.limit ?? 20);
82
+
83
+ if (errors.length === 0) {
84
+ return {
85
+ content: [{
86
+ type: 'text',
87
+ text: 'No errors or warnings found.',
88
+ }],
89
+ };
90
+ }
91
+
92
+ const formatted = errors.map(log => {
93
+ const icon = log.type === 'error' ? '❌' : '⚠️';
94
+ const parts = [
95
+ `${icon} [${log.timestamp}]`,
96
+ `[${log.type.toUpperCase()}]`,
97
+ ];
98
+ if (log.issuer) parts.push(`[${log.issuer}]`);
99
+ parts.push(log.message);
100
+ if (log.file) parts.push(`\n File: ${log.file}`);
101
+ if (log.stack) parts.push(`\n Stack: ${log.stack}`);
102
+ return parts.join(' ');
103
+ }).join('\n\n');
104
+
105
+ const errorCount = errors.filter(e => e.type === 'error').length;
106
+ const warnCount = errors.filter(e => e.type === 'warn').length;
107
+
108
+ return {
109
+ content: [{
110
+ type: 'text',
111
+ text: `Found ${errorCount} error(s) and ${warnCount} warning(s):\n\n${formatted}`,
112
+ }],
113
+ };
114
+ }
115
+ );
116
+
117
+ // Tool: clear_logs
118
+ server.tool(
119
+ 'clear_logs',
120
+ 'Clear all logs from the in-memory buffer',
121
+ {},
122
+ async () => {
123
+ const count = store.count;
124
+ store.clear();
125
+
126
+ return {
127
+ content: [{
128
+ type: 'text',
129
+ text: `Cleared ${count} log(s) from buffer.`,
130
+ }],
131
+ };
132
+ }
133
+ );
134
+
135
+ // Tool: get_status
136
+ server.tool(
137
+ 'get_status',
138
+ 'Get the current status of the log watcher',
139
+ {},
140
+ async () => {
141
+ const status = {
142
+ watching: watcher.isWatching,
143
+ filePath: watcher.path,
144
+ fileExists: watcher.fileExists,
145
+ logCount: store.count,
146
+ errorCount: store.countByType('error'),
147
+ warningCount: store.countByType('warn'),
148
+ lastUpdate: store.lastTimestamp,
149
+ };
150
+
151
+ const lines = [
152
+ `Watcher Status:`,
153
+ ` Watching: ${status.watching ? 'Yes' : 'No'}`,
154
+ ` Log File: ${status.filePath}`,
155
+ ` File Exists: ${status.fileExists ? 'Yes' : 'No'}`,
156
+ ``,
157
+ `Log Statistics:`,
158
+ ` Total Logs: ${status.logCount}`,
159
+ ` Errors: ${status.errorCount}`,
160
+ ` Warnings: ${status.warningCount}`,
161
+ ` Last Update: ${status.lastUpdate ?? 'Never'}`,
162
+ ];
163
+
164
+ return {
165
+ content: [{
166
+ type: 'text',
167
+ text: lines.join('\n'),
168
+ }],
169
+ };
170
+ }
171
+ );
172
+
173
+ // Start the server
174
+ async function main() {
175
+ // Start watching the log file
176
+ await watcher.start();
177
+
178
+ // Connect via stdio
179
+ const transport = new StdioServerTransport();
180
+ await server.connect(transport);
181
+
182
+ // Handle shutdown
183
+ process.on('SIGINT', async () => {
184
+ await watcher.stop();
185
+ process.exit(0);
186
+ });
187
+
188
+ process.on('SIGTERM', async () => {
189
+ await watcher.stop();
190
+ process.exit(0);
191
+ });
192
+ }
193
+
194
+ main().catch((error) => {
195
+ console.error('Failed to start MCP server:', error);
196
+ process.exit(1);
197
+ });
@@ -0,0 +1,113 @@
1
+ import type { LogEntry, LogFilter, LogType } from './types.js';
2
+
3
+ /**
4
+ * In-memory circular buffer for storing log entries.
5
+ */
6
+ export class LogStore {
7
+ private logs: LogEntry[] = [];
8
+ private maxSize: number;
9
+
10
+ constructor(maxSize: number = 1000) {
11
+ this.maxSize = maxSize;
12
+ }
13
+
14
+ /**
15
+ * Add a log entry to the store.
16
+ * Removes oldest entries if buffer is full.
17
+ */
18
+ add(entry: LogEntry): void {
19
+ this.logs.push(entry);
20
+
21
+ // Trim if over max size
22
+ if (this.logs.length > this.maxSize) {
23
+ this.logs = this.logs.slice(-this.maxSize);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Add multiple log entries at once.
29
+ */
30
+ addMany(entries: LogEntry[]): void {
31
+ for (const entry of entries) {
32
+ this.add(entry);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get logs with optional filtering.
38
+ */
39
+ get(filter?: LogFilter): LogEntry[] {
40
+ let result = [...this.logs];
41
+
42
+ if (filter?.types && filter.types.length > 0) {
43
+ result = result.filter(log => filter.types!.includes(log.type));
44
+ }
45
+
46
+ if (filter?.since) {
47
+ const sinceDate = new Date(filter.since);
48
+ result = result.filter(log => new Date(log.timestamp) >= sinceDate);
49
+ }
50
+
51
+ if (filter?.issuer) {
52
+ const issuerLower = filter.issuer.toLowerCase();
53
+ result = result.filter(log =>
54
+ log.issuer?.toLowerCase().includes(issuerLower)
55
+ );
56
+ }
57
+
58
+ if (filter?.search) {
59
+ const searchLower = filter.search.toLowerCase();
60
+ result = result.filter(log =>
61
+ log.message.toLowerCase().includes(searchLower) ||
62
+ log.file?.toLowerCase().includes(searchLower) ||
63
+ log.request?.toLowerCase().includes(searchLower)
64
+ );
65
+ }
66
+
67
+ if (filter?.limit && filter.limit > 0) {
68
+ // Return most recent logs
69
+ result = result.slice(-filter.limit);
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ /**
76
+ * Get only errors and warnings.
77
+ */
78
+ getErrors(limit?: number): LogEntry[] {
79
+ return this.get({
80
+ types: ['error', 'warn'],
81
+ limit,
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Count logs by type.
87
+ */
88
+ countByType(type: LogType): number {
89
+ return this.logs.filter(log => log.type === type).length;
90
+ }
91
+
92
+ /**
93
+ * Get total log count.
94
+ */
95
+ get count(): number {
96
+ return this.logs.length;
97
+ }
98
+
99
+ /**
100
+ * Get the timestamp of the most recent log.
101
+ */
102
+ get lastTimestamp(): string | null {
103
+ if (this.logs.length === 0) return null;
104
+ return this.logs[this.logs.length - 1].timestamp;
105
+ }
106
+
107
+ /**
108
+ * Clear all logs from the store.
109
+ */
110
+ clear(): void {
111
+ this.logs = [];
112
+ }
113
+ }