@just-every/mcp-read-website-fast 0.1.9 → 0.1.11

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 CHANGED
@@ -86,7 +86,7 @@ Drop this into your client’s mcp.json (e.g. .vscode/mcp.json, ~/.cursor/mcp.js
86
86
 
87
87
  ### Available Tools
88
88
 
89
- - `read_website_fast` - Fetches a webpage and converts it to clean markdown
89
+ - `read_website` - Fetches a webpage and converts it to clean markdown
90
90
  - Parameters:
91
91
  - `url` (required): The HTTP/HTTPS URL to fetch
92
92
  - `depth` (optional): Crawl depth (0 = single page)
@@ -144,6 +144,22 @@ npm run dev fetch https://example.com --output both
144
144
  npm run dev clear-cache
145
145
  ```
146
146
 
147
+ ## Auto-Restart Feature
148
+
149
+ The MCP server includes automatic restart capability by default for improved reliability:
150
+
151
+ - Automatically restarts the server if it crashes
152
+ - Handles unhandled exceptions and promise rejections
153
+ - Implements exponential backoff (max 10 attempts in 1 minute)
154
+ - Logs all restart attempts for monitoring
155
+ - Gracefully handles shutdown signals (SIGINT, SIGTERM)
156
+
157
+ For development/debugging without auto-restart:
158
+ ```bash
159
+ # Run directly without restart wrapper
160
+ npm run serve:dev
161
+ ```
162
+
147
163
  ## Architecture
148
164
 
149
165
  ```
@@ -153,7 +169,9 @@ mcp/
153
169
  │ ├── parser/ # DOM parsing, Readability, Turndown conversion
154
170
  │ ├── cache/ # Disk-based caching with SHA-256 keys
155
171
  │ ├── utils/ # Logger, chunker utilities
156
- └── index.ts # CLI entry point
172
+ ├── index.ts # CLI entry point
173
+ │ ├── serve.ts # MCP server entry point
174
+ │ └── serve-restart.ts # Auto-restart wrapper
157
175
  ```
158
176
 
159
177
  ## Development
@@ -18,7 +18,7 @@ async function main() {
18
18
  if (distExists) {
19
19
  // Use compiled JavaScript for production (fast startup)
20
20
  if (command === 'serve') {
21
- const servePath = join(__dirname, '..', 'dist', 'serve.js');
21
+ const servePath = join(__dirname, '..', 'dist', 'serve-restart.js');
22
22
  await import(servePath);
23
23
  } else {
24
24
  const cliPath = join(__dirname, '..', 'dist', 'index.js');
@@ -30,7 +30,7 @@ async function main() {
30
30
  await import('tsx/esm');
31
31
 
32
32
  if (command === 'serve') {
33
- const servePath = join(__dirname, '..', 'src', 'serve.ts');
33
+ const servePath = join(__dirname, '..', 'src', 'serve-restart.ts');
34
34
  await import(servePath);
35
35
  } else {
36
36
  const cliPath = join(__dirname, '..', 'src', 'index.ts');
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const MAX_RESTART_ATTEMPTS = 10;
7
+ const RESTART_WINDOW_MS = 60000;
8
+ const INITIAL_BACKOFF_MS = 1000;
9
+ const MAX_BACKOFF_MS = 30000;
10
+ let restartAttempts = [];
11
+ let currentBackoff = INITIAL_BACKOFF_MS;
12
+ const log = (level, message, ...args) => {
13
+ const timestamp = new Date().toISOString();
14
+ console.error(`[${timestamp}] [${level}] [restart-wrapper]`, message, ...args);
15
+ };
16
+ const cleanupRestartAttempts = () => {
17
+ const now = Date.now();
18
+ restartAttempts = restartAttempts.filter(timestamp => now - timestamp < RESTART_WINDOW_MS);
19
+ };
20
+ const shouldRestart = () => {
21
+ cleanupRestartAttempts();
22
+ if (restartAttempts.length >= MAX_RESTART_ATTEMPTS) {
23
+ log('ERROR', `Reached maximum restart attempts (${MAX_RESTART_ATTEMPTS}) within ${RESTART_WINDOW_MS}ms`);
24
+ return false;
25
+ }
26
+ return true;
27
+ };
28
+ const getBackoffDelay = () => {
29
+ const delay = Math.min(currentBackoff, MAX_BACKOFF_MS);
30
+ currentBackoff = Math.min(currentBackoff * 2, MAX_BACKOFF_MS);
31
+ return delay;
32
+ };
33
+ const resetBackoff = () => {
34
+ currentBackoff = INITIAL_BACKOFF_MS;
35
+ };
36
+ const startServer = () => {
37
+ log('INFO', 'Starting MCP server...');
38
+ const serverPath = join(__dirname, 'serve.js');
39
+ const child = spawn(process.execPath, [serverPath], {
40
+ stdio: 'inherit',
41
+ env: process.env,
42
+ });
43
+ let shuttingDown = false;
44
+ let restartTimer = null;
45
+ let startupTimer = setTimeout(() => {
46
+ log('INFO', 'Server started successfully');
47
+ resetBackoff();
48
+ }, 5000);
49
+ child.on('exit', (code, signal) => {
50
+ clearTimeout(startupTimer);
51
+ if (shuttingDown) {
52
+ log('INFO', 'Server stopped gracefully');
53
+ process.exit(0);
54
+ return;
55
+ }
56
+ if (code === 0) {
57
+ log('INFO', 'Server exited cleanly');
58
+ process.exit(0);
59
+ return;
60
+ }
61
+ log('WARN', `Server exited with code ${code}, signal ${signal}`);
62
+ if (!shouldRestart()) {
63
+ log('ERROR', 'Too many restart attempts, giving up');
64
+ process.exit(1);
65
+ return;
66
+ }
67
+ const backoffDelay = getBackoffDelay();
68
+ restartAttempts.push(Date.now());
69
+ log('INFO', `Restarting server in ${backoffDelay}ms (attempt ${restartAttempts.length}/${MAX_RESTART_ATTEMPTS})...`);
70
+ restartTimer = setTimeout(() => {
71
+ startServer();
72
+ }, backoffDelay);
73
+ });
74
+ child.on('error', (error) => {
75
+ log('ERROR', 'Failed to start server:', error);
76
+ process.exit(1);
77
+ });
78
+ const shutdown = (signal) => {
79
+ if (shuttingDown)
80
+ return;
81
+ shuttingDown = true;
82
+ log('INFO', `Received ${signal}, shutting down...`);
83
+ if (restartTimer) {
84
+ clearTimeout(restartTimer);
85
+ }
86
+ child.kill(signal);
87
+ setTimeout(() => {
88
+ log('WARN', 'Force killing child process');
89
+ child.kill('SIGKILL');
90
+ }, 5000);
91
+ };
92
+ process.on('SIGINT', () => shutdown('SIGINT'));
93
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
94
+ };
95
+ process.on('uncaughtException', (error) => {
96
+ log('ERROR', 'Uncaught exception in restart wrapper:', error);
97
+ process.exit(1);
98
+ });
99
+ process.on('unhandledRejection', (reason) => {
100
+ log('ERROR', 'Unhandled rejection in restart wrapper:', reason);
101
+ process.exit(1);
102
+ });
103
+ log('INFO', 'MCP server restart wrapper starting...');
104
+ log('INFO', `Configuration: max attempts=${MAX_RESTART_ATTEMPTS}, window=${RESTART_WINDOW_MS}ms`);
105
+ startServer();
package/dist/serve.js CHANGED
@@ -1,13 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ console.error('[serve.ts] Process started, PID:', process.pid);
3
+ console.error('[serve.ts] Node version:', process.version);
4
+ console.error('[serve.ts] Current directory:', process.cwd());
2
5
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
7
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
+ import { logger, LogLevel } from './utils/logger.js';
9
+ logger.setLevel(LogLevel.DEBUG);
10
+ logger.info('MCP Server starting up...');
11
+ logger.debug('Node version:', process.version);
12
+ logger.debug('Working directory:', process.cwd());
13
+ logger.debug('Environment:', { LOG_LEVEL: process.env.LOG_LEVEL });
5
14
  process.stdin.on('error', () => { });
6
15
  process.stdout.on('error', () => { });
7
16
  process.stderr.on('error', () => { });
8
17
  let fetchMarkdownModule;
9
18
  let fsPromises;
10
19
  let pathModule;
20
+ logger.debug('Creating MCP server instance...');
11
21
  const server = new Server({
12
22
  name: 'read-website-fast',
13
23
  version: '0.1.0',
@@ -17,12 +27,13 @@ const server = new Server({
17
27
  resources: {},
18
28
  },
19
29
  });
30
+ logger.info('MCP server instance created successfully');
20
31
  server.onerror = error => {
21
- console.error('[MCP Server Error]', error);
32
+ logger.error('MCP Server Error:', error);
22
33
  };
23
34
  const READ_WEBSITE_TOOL = {
24
- name: 'read_website_fast',
25
- description: 'Quickly reads webpages and converts to markdown for fast, token efficient web scraping',
35
+ name: 'read_website',
36
+ description: 'Fast, token-efficient web content extraction - ideal for reading documentation, analyzing content, and gathering information from websites. Converts to clean Markdown while preserving links and structure.',
26
37
  inputSchema: {
27
38
  type: 'object',
28
39
  properties: {
@@ -43,6 +54,13 @@ const READ_WEBSITE_TOOL = {
43
54
  },
44
55
  required: ['url'],
45
56
  },
57
+ annotations: {
58
+ title: 'Read Website',
59
+ readOnlyHint: true,
60
+ destructiveHint: false,
61
+ idempotentHint: true,
62
+ openWorldHint: true,
63
+ },
46
64
  };
47
65
  const RESOURCES = [
48
66
  {
@@ -58,25 +76,44 @@ const RESOURCES = [
58
76
  description: 'Clear the cache directory',
59
77
  },
60
78
  ];
61
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
62
- tools: [READ_WEBSITE_TOOL],
63
- }));
79
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
80
+ logger.debug('Received ListTools request');
81
+ const response = {
82
+ tools: [READ_WEBSITE_TOOL],
83
+ };
84
+ logger.debug('Returning tools:', response.tools.map(t => t.name));
85
+ return response;
86
+ });
64
87
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
65
- if (request.params.name !== 'read_website_fast') {
66
- throw new Error(`Unknown tool: ${request.params.name}`);
88
+ logger.info('Received CallTool request:', request.params.name);
89
+ logger.debug('Request params:', JSON.stringify(request.params, null, 2));
90
+ if (request.params.name !== 'read_website') {
91
+ const error = `Unknown tool: ${request.params.name}`;
92
+ logger.error(error);
93
+ throw new Error(error);
67
94
  }
68
95
  try {
69
96
  if (!fetchMarkdownModule) {
97
+ logger.debug('Lazy loading fetchMarkdown module...');
70
98
  fetchMarkdownModule = await import('./internal/fetchMarkdown.js');
99
+ logger.info('fetchMarkdown module loaded successfully');
71
100
  }
72
101
  const args = request.params.arguments;
73
102
  if (!args.url || typeof args.url !== 'string') {
74
103
  throw new Error('URL parameter is required and must be a string');
75
104
  }
105
+ logger.info(`Processing read request for URL: ${args.url}`);
106
+ logger.debug('Read parameters:', {
107
+ url: args.url,
108
+ depth: args.depth,
109
+ respectRobots: args.respectRobots,
110
+ });
111
+ logger.debug('Calling fetchMarkdown...');
76
112
  const result = await fetchMarkdownModule.fetchMarkdown(args.url, {
77
113
  depth: args.depth ?? 0,
78
114
  respectRobots: args.respectRobots ?? true,
79
115
  });
116
+ logger.info('Content fetched successfully');
80
117
  if (result.error && result.markdown) {
81
118
  return {
82
119
  content: [
@@ -95,14 +132,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
95
132
  };
96
133
  }
97
134
  catch (error) {
98
- console.error('Tool execution error:', error);
135
+ logger.error('Error fetching content:', error.message);
136
+ logger.debug('Error stack:', error.stack);
137
+ logger.debug('Error details:', {
138
+ name: error.name,
139
+ code: error.code,
140
+ ...error,
141
+ });
99
142
  throw new Error(`Failed to fetch content: ${error instanceof Error ? error.message : 'Unknown error'}`);
100
143
  }
101
144
  });
102
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
103
- resources: RESOURCES,
104
- }));
145
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
146
+ logger.debug('Received ListResources request');
147
+ return {
148
+ resources: RESOURCES,
149
+ };
150
+ });
105
151
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
152
+ logger.debug('Received ReadResource request:', request.params);
106
153
  const uri = request.params.uri;
107
154
  if (!fsPromises) {
108
155
  fsPromises = await import('fs/promises');
@@ -190,49 +237,84 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
190
237
  throw new Error(`Unknown resource: ${uri}`);
191
238
  });
192
239
  async function runServer() {
193
- const transport = new StdioServerTransport();
194
- transport.onerror = error => {
195
- console.error('[Transport Error]', error);
196
- };
197
- process.on('SIGINT', async () => {
198
- console.error('Received SIGINT, shutting down gracefully...');
199
- await server.close();
200
- process.exit(0);
201
- });
202
- process.on('SIGTERM', async () => {
203
- console.error('Received SIGTERM, shutting down gracefully...');
204
- await server.close();
205
- process.exit(0);
206
- });
207
- process.on('uncaughtException', error => {
208
- console.error('Uncaught exception:', error);
209
- if (error && error.message && error.message.includes('EPIPE')) {
210
- console.error('Pipe error detected, keeping server alive');
211
- return;
212
- }
213
- process.exit(1);
214
- });
215
- process.on('unhandledRejection', (reason, promise) => {
216
- console.error('Unhandled rejection at:', promise, 'reason:', reason);
217
- });
218
- process.stdin.on('end', () => {
219
- console.error('Stdin closed, shutting down...');
220
- process.exit(0);
221
- });
222
- process.stdin.on('error', error => {
223
- console.error('Stdin error:', error);
224
- });
225
240
  try {
241
+ logger.info('Starting MCP server...');
242
+ logger.debug('Creating StdioServerTransport...');
243
+ const transport = new StdioServerTransport();
244
+ logger.debug('Transport created, connecting to server...');
245
+ transport.onerror = error => {
246
+ logger.error('Transport Error:', error);
247
+ if (error?.message?.includes('Connection closed')) {
248
+ logger.info('Connection closed by client');
249
+ process.exit(0);
250
+ }
251
+ };
252
+ const cleanup = async (signal) => {
253
+ logger.info(`Received ${signal}, shutting down gracefully...`);
254
+ try {
255
+ await server.close();
256
+ logger.info('Server closed successfully');
257
+ process.exit(0);
258
+ }
259
+ catch (error) {
260
+ logger.error('Error during cleanup:', error);
261
+ process.exit(1);
262
+ }
263
+ };
264
+ process.on('SIGINT', () => cleanup('SIGINT'));
265
+ process.on('SIGTERM', () => cleanup('SIGTERM'));
266
+ process.on('uncaughtException', error => {
267
+ logger.error('Uncaught exception:', error.message);
268
+ logger.error('Stack trace:', error.stack);
269
+ logger.debug('Full error object:', error);
270
+ if (error && error.message && error.message.includes('EPIPE')) {
271
+ logger.warn('Pipe error detected, keeping server alive');
272
+ return;
273
+ }
274
+ process.exit(1);
275
+ });
276
+ process.on('unhandledRejection', (reason, promise) => {
277
+ logger.error('Unhandled Rejection at:', promise);
278
+ logger.error('Rejection reason:', reason);
279
+ logger.debug('Full rejection details:', { reason, promise });
280
+ });
281
+ process.on('exit', code => {
282
+ logger.info(`Process exiting with code: ${code}`);
283
+ });
284
+ process.on('warning', warning => {
285
+ logger.warn('Process warning:', warning.message);
286
+ logger.debug('Warning details:', warning);
287
+ });
288
+ process.stdin.on('end', () => {
289
+ logger.info('Stdin closed, shutting down...');
290
+ setTimeout(() => process.exit(0), 100);
291
+ });
292
+ process.stdin.on('error', error => {
293
+ logger.warn('Stdin error:', error);
294
+ });
226
295
  await server.connect(transport);
227
- console.error('read-website-fast MCP server running');
296
+ logger.info('MCP server connected and running successfully!');
297
+ logger.info('Ready to receive requests');
298
+ logger.debug('Server details:', {
299
+ name: 'read-website-fast',
300
+ version: '0.1.0',
301
+ pid: process.pid,
302
+ });
303
+ setInterval(() => {
304
+ logger.debug('Server heartbeat - still running...');
305
+ }, 30000);
228
306
  process.stdin.resume();
229
307
  }
230
308
  catch (error) {
231
- console.error('Failed to start server:', error);
232
- process.exit(1);
309
+ logger.error('Failed to start server:', error.message);
310
+ logger.debug('Startup error details:', error);
311
+ throw error;
233
312
  }
234
313
  }
314
+ logger.info('Initializing MCP server...');
235
315
  runServer().catch(error => {
236
- console.error('Server initialization error:', error);
316
+ logger.error('Fatal server error:', error.message);
317
+ logger.error('Stack trace:', error.stack);
318
+ logger.debug('Full error:', error);
237
319
  process.exit(1);
238
320
  });
@@ -23,10 +23,10 @@ export class Logger {
23
23
  console.error(prefix, message, ...args);
24
24
  break;
25
25
  case LogLevel.WARN:
26
- console.warn(prefix, message, ...args);
26
+ console.error(prefix, message, ...args);
27
27
  break;
28
28
  default:
29
- console.log(prefix, message, ...args);
29
+ console.error(prefix, message, ...args);
30
30
  }
31
31
  }
32
32
  error(message, ...args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@just-every/mcp-read-website-fast",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Markdown Content Preprocessor - Fetch web pages, extract content, convert to clean Markdown",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "build:dev": "tsc",
19
19
  "dev": "tsx src/index.ts",
20
20
  "start": "node dist/index.js",
21
- "serve": "tsx src/serve.ts",
21
+ "serve": "node dist/serve-restart.js",
22
22
  "serve:dev": "tsx src/serve.ts",
23
23
  "test": "vitest",
24
24
  "test:deploy": "vitest run test/deployment.test.ts",
@@ -50,7 +50,7 @@
50
50
  "homepage": "https://github.com/just-every/mcp-read-website-fast#readme",
51
51
  "license": "MIT",
52
52
  "dependencies": {
53
- "@modelcontextprotocol/sdk": "^1.12.1",
53
+ "@modelcontextprotocol/sdk": "^1.12.3",
54
54
  "@mozilla/readability": "^0.6.0",
55
55
  "commander": "^14.0.0",
56
56
  "jsdom": "^26.1.0",