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.
- package/README.md +116 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/dist/log-store.d.ts +43 -0
- package/dist/log-store.d.ts.map +1 -0
- package/dist/log-store.js +93 -0
- package/dist/log-store.js.map +1 -0
- package/dist/log-watcher.d.ts +45 -0
- package/dist/log-watcher.d.ts.map +1 -0
- package/dist/log-watcher.js +164 -0
- package/dist/log-watcher.js.map +1 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -0
- package/src/config.ts +37 -0
- package/src/index.ts +197 -0
- package/src/log-store.ts +113 -0
- package/src/log-watcher.ts +186 -0
- package/src/types.ts +53 -0
- package/tsconfig.json +18 -0
|
@@ -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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
});
|
package/src/log-store.ts
ADDED
|
@@ -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
|
+
}
|