@jagilber-org/index-server 1.26.5 → 1.27.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/CHANGELOG.md +166 -110
- package/dist/config/dashboardConfig.d.ts +11 -4
- package/dist/config/dashboardConfig.js +1 -4
- package/dist/dashboard/client/admin.html +26 -26
- package/dist/dashboard/server/AdminPanelConfig.d.ts +1 -2
- package/dist/dashboard/server/AdminPanelConfig.js +1 -2
- package/dist/dashboard/server/ApiRoutes.d.ts +5 -4
- package/dist/dashboard/server/ApiRoutes.js +40 -35
- package/dist/dashboard/server/routes/index.js +11 -9
- package/dist/lib/mcpStdioLogging.d.ts +12 -0
- package/dist/lib/mcpStdioLogging.js +39 -0
- package/dist/services/handlers.dashboardConfig.js +1 -2
- package/dist/services/indexContext.js +5 -6
- package/dist/services/logger.js +28 -13
- package/package.json +2 -1
- package/schemas/index-server.code-schema.json +7424 -1588
- package/schemas/manifest.json +3 -3
- package/scripts/setup-wizard.mjs +268 -16
- package/server.json +2 -2
|
@@ -15,10 +15,17 @@ interface DashboardHttpConfig {
|
|
|
15
15
|
verboseLogging: boolean;
|
|
16
16
|
mutationEnabled: boolean;
|
|
17
17
|
adminApiKey?: string;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Dashboard HTTP API rate limit, in requests per minute.
|
|
20
|
+
*
|
|
21
|
+
* - `0` (default) disables rate limiting entirely (HTTP API + usage tracking).
|
|
22
|
+
* - Any positive integer N enforces N requests/min globally with a fixed
|
|
23
|
+
* 60-second window. Bulk import/export/backup/restore routes are
|
|
24
|
+
* unconditionally exempt regardless of this value.
|
|
25
|
+
*
|
|
26
|
+
* Configured via the `INDEX_SERVER_RATE_LIMIT` environment variable.
|
|
27
|
+
*/
|
|
28
|
+
rateLimitPerMinute: number;
|
|
22
29
|
tls: DashboardTlsConfig;
|
|
23
30
|
}
|
|
24
31
|
interface DashboardAdminConfig {
|
|
@@ -36,10 +36,7 @@ function parseDashboardConfig(mutationEnabled, instructionsBaseDir) {
|
|
|
36
36
|
verboseLogging: (0, envUtils_1.getBooleanEnv)('INDEX_SERVER_VERBOSE_LOGGING'),
|
|
37
37
|
mutationEnabled,
|
|
38
38
|
adminApiKey: process.env.INDEX_SERVER_ADMIN_API_KEY || undefined,
|
|
39
|
-
|
|
40
|
-
rateLimitWindowMs: Math.max(1, (0, configUtils_1.numberFromEnv)('INDEX_SERVER_RATE_LIMIT_WINDOW_MS', 60_000)),
|
|
41
|
-
rateLimitMax: Math.max(0, (0, configUtils_1.numberFromEnv)('INDEX_SERVER_RATE_LIMIT_MAX', 100)),
|
|
42
|
-
rateLimitMutationMax: Math.max(0, (0, configUtils_1.numberFromEnv)('INDEX_SERVER_RATE_LIMIT_MUTATION_MAX', 20)),
|
|
39
|
+
rateLimitPerMinute: Math.max(0, (0, configUtils_1.numberFromEnv)('INDEX_SERVER_RATE_LIMIT', 0)),
|
|
43
40
|
tls: {
|
|
44
41
|
enabled: (0, envUtils_1.getBooleanEnv)('INDEX_SERVER_DASHBOARD_TLS'),
|
|
45
42
|
certPath: process.env.INDEX_SERVER_DASHBOARD_TLS_CERT || undefined,
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<meta name="dashboard-build-version" content="1.
|
|
4
|
+
<meta name="dashboard-build-version" content="1.27.0-9badd8dd">
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>Index Server Admin</title>
|
|
8
|
-
<link rel="stylesheet" href="css/admin.css?v=1.
|
|
9
|
-
<script defer src="js/admin.utils.js?v=1.
|
|
10
|
-
<script defer src="js/admin.auth.js?v=1.
|
|
11
|
-
<script defer src="js/admin.overview.js?v=1.
|
|
12
|
-
<script defer src="js/admin.sessions.js?v=1.
|
|
13
|
-
<script defer src="js/admin.monitor.js?v=1.
|
|
14
|
-
<script defer src="js/admin.graph.js?v=1.
|
|
8
|
+
<link rel="stylesheet" href="css/admin.css?v=1.27.0-9badd8dd">
|
|
9
|
+
<script defer src="js/admin.utils.js?v=1.27.0-9badd8dd"></script>
|
|
10
|
+
<script defer src="js/admin.auth.js?v=1.27.0-9badd8dd"></script>
|
|
11
|
+
<script defer src="js/admin.overview.js?v=1.27.0-9badd8dd"></script>
|
|
12
|
+
<script defer src="js/admin.sessions.js?v=1.27.0-9badd8dd"></script>
|
|
13
|
+
<script defer src="js/admin.monitor.js?v=1.27.0-9badd8dd"></script>
|
|
14
|
+
<script defer src="js/admin.graph.js?v=1.27.0-9badd8dd"></script>
|
|
15
15
|
<script defer src="js/marked.umd.js"></script>
|
|
16
|
-
<script defer src="js/admin.instructions.js?v=1.
|
|
17
|
-
<script defer src="js/admin.logs.js?v=1.
|
|
18
|
-
<script defer src="js/admin.maintenance.js?v=1.
|
|
19
|
-
<script defer src="js/admin.config.js?v=1.
|
|
20
|
-
<script defer src="js/admin.performance.js?v=1.
|
|
21
|
-
<script defer src="js/admin.instances.js?v=1.
|
|
22
|
-
<script defer src="js/admin.embeddings.js?v=1.
|
|
23
|
-
<script defer src="js/admin.messaging.js?v=1.
|
|
24
|
-
<script defer src="js/admin.sqlite.js?v=1.
|
|
25
|
-
<script defer src="js/admin.boot.js?v=1.
|
|
26
|
-
<script defer src="js/admin.feedback.js?v=1.
|
|
16
|
+
<script defer src="js/admin.instructions.js?v=1.27.0-9badd8dd"></script>
|
|
17
|
+
<script defer src="js/admin.logs.js?v=1.27.0-9badd8dd"></script>
|
|
18
|
+
<script defer src="js/admin.maintenance.js?v=1.27.0-9badd8dd"></script>
|
|
19
|
+
<script defer src="js/admin.config.js?v=1.27.0-9badd8dd"></script>
|
|
20
|
+
<script defer src="js/admin.performance.js?v=1.27.0-9badd8dd"></script>
|
|
21
|
+
<script defer src="js/admin.instances.js?v=1.27.0-9badd8dd"></script>
|
|
22
|
+
<script defer src="js/admin.embeddings.js?v=1.27.0-9badd8dd"></script>
|
|
23
|
+
<script defer src="js/admin.messaging.js?v=1.27.0-9badd8dd"></script>
|
|
24
|
+
<script defer src="js/admin.sqlite.js?v=1.27.0-9badd8dd"></script>
|
|
25
|
+
<script defer src="js/admin.boot.js?v=1.27.0-9badd8dd"></script>
|
|
26
|
+
<script defer src="js/admin.feedback.js?v=1.27.0-9badd8dd"></script>
|
|
27
27
|
</head>
|
|
28
28
|
<body>
|
|
29
29
|
<div class="admin-container admin-root">
|
|
@@ -879,10 +879,10 @@
|
|
|
879
879
|
}
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
-
// Graph logic was extracted to js/admin.graph.js?v=1.
|
|
882
|
+
// Graph logic was extracted to js/admin.graph.js?v=1.27.0-9badd8dd
|
|
883
883
|
// Functions available globally: reloadGraphMermaid, initGraphScopeDefaults, copyMermaidSource, toggleGraphEdit, applyGraphEdit, cancelGraphEdit, refreshDrillCategories, loadDrillInstructions, clearSelections
|
|
884
884
|
|
|
885
|
-
<!-- overview functions moved to js/admin.overview.js?v=1.
|
|
885
|
+
<!-- overview functions moved to js/admin.overview.js?v=1.27.0-9badd8dd -->
|
|
886
886
|
|
|
887
887
|
// Lightweight overview-level maintenance display (optional)
|
|
888
888
|
// Intentionally minimal to avoid blocking overview rendering.
|
|
@@ -1067,7 +1067,7 @@
|
|
|
1067
1067
|
}
|
|
1068
1068
|
|
|
1069
1069
|
// --- Backup / Restore ---
|
|
1070
|
-
// Extracted to js/admin.maintenance.js?v=1.
|
|
1070
|
+
// Extracted to js/admin.maintenance.js?v=1.27.0-9badd8dd
|
|
1071
1071
|
|
|
1072
1072
|
async function performBackup() {
|
|
1073
1073
|
try {
|
|
@@ -1133,7 +1133,7 @@
|
|
|
1133
1133
|
}
|
|
1134
1134
|
|
|
1135
1135
|
async function loadConfiguration() {
|
|
1136
|
-
// Primary implementation in js/admin.config.js?v=1.
|
|
1136
|
+
// Primary implementation in js/admin.config.js?v=1.27.0-9badd8dd (loaded via defer).
|
|
1137
1137
|
// This inline fallback only fires if the external script failed to load.
|
|
1138
1138
|
if (window.__configExternalLoaded) return;
|
|
1139
1139
|
try {
|
|
@@ -1193,10 +1193,10 @@
|
|
|
1193
1193
|
return false;
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
1196
|
-
// Monitoring functions moved to js/admin.monitor.js?v=1.
|
|
1196
|
+
// Monitoring functions moved to js/admin.monitor.js?v=1.27.0-9badd8dd
|
|
1197
1197
|
|
|
1198
1198
|
// ===== Log Viewer =====
|
|
1199
|
-
// Extracted to js/admin.logs.js?v=1.
|
|
1199
|
+
// Extracted to js/admin.logs.js?v=1.27.0-9badd8dd
|
|
1200
1200
|
|
|
1201
1201
|
// ===== Instruction Management =====
|
|
1202
1202
|
let instructionEditing = null;
|
|
@@ -1693,7 +1693,7 @@
|
|
|
1693
1693
|
setInterval(fetchResourceTrends, 10000);
|
|
1694
1694
|
})();
|
|
1695
1695
|
|
|
1696
|
-
// Instruction management logic extracted to js/admin.instructions.js?v=1.
|
|
1696
|
+
// Instruction management logic extracted to js/admin.instructions.js?v=1.27.0-9badd8dd
|
|
1697
1697
|
// Functions exposed globally: loadInstructions, renderInstructionList, editInstruction, saveInstruction, deleteInstruction, etc.
|
|
1698
1698
|
|
|
1699
1699
|
function startAutoRefresh() {
|
|
@@ -23,8 +23,7 @@ class AdminPanelConfig {
|
|
|
23
23
|
enableVerboseLogging: !!serverHttp?.verboseLogging,
|
|
24
24
|
enableMutation: runtimeConfig.mutation.enabled,
|
|
25
25
|
rateLimit: {
|
|
26
|
-
|
|
27
|
-
maxRequests: 100
|
|
26
|
+
perMinute: serverHttp?.rateLimitPerMinute ?? 0
|
|
28
27
|
}
|
|
29
28
|
},
|
|
30
29
|
indexSettings: {
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
import { Router } from 'express';
|
|
9
9
|
export interface ApiRoutesOptions {
|
|
10
10
|
enableCors?: boolean;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Optional override for the rate-limit (requests per 60s window).
|
|
13
|
+
* `0` disables. When omitted, falls back to `httpCfg.rateLimitPerMinute`.
|
|
14
|
+
*/
|
|
15
|
+
rateLimitPerMinute?: number;
|
|
15
16
|
}
|
|
16
17
|
export declare function createApiRoutes(options?: ApiRoutesOptions): Router;
|
|
17
18
|
export default createApiRoutes;
|
|
@@ -49,11 +49,41 @@ const runtimeConfig_js_1 = require("../../config/runtimeConfig.js");
|
|
|
49
49
|
const index_js_1 = require("./routes/index.js");
|
|
50
50
|
const ensureLoadedMiddleware_js_1 = require("./middleware/ensureLoadedMiddleware.js");
|
|
51
51
|
const logger_js_1 = require("../../services/logger.js");
|
|
52
|
+
/**
|
|
53
|
+
* Path prefixes whose endpoints are *unconditionally* exempt from the dashboard
|
|
54
|
+
* HTTP rate limiter. These are bulk-shaped, operator-driven operations
|
|
55
|
+
* (backup, restore, import/export, normalize) where the size of a single
|
|
56
|
+
* request — not the *frequency* of requests — is the natural cost driver.
|
|
57
|
+
*
|
|
58
|
+
* Matching is done with `req.path.startsWith(prefix)` against the path *as
|
|
59
|
+
* mounted on the /api router* (so prefixes do not include the leading "/api").
|
|
60
|
+
*
|
|
61
|
+
* Note: the MCP tool surface (index_import / index_export / promote_from_repo
|
|
62
|
+
* / restore handlers) is already outside this HTTP limiter, so no entries are
|
|
63
|
+
* needed for those.
|
|
64
|
+
*/
|
|
65
|
+
const BULK_EXEMPT_PREFIXES = [
|
|
66
|
+
'/admin/maintenance/normalize',
|
|
67
|
+
'/admin/maintenance/backup', // covers backup, backup/import, backup/:id, backup/:id/export
|
|
68
|
+
'/admin/maintenance/backups', // covers backups, backups/prune
|
|
69
|
+
'/admin/maintenance/restore',
|
|
70
|
+
'/charts/export',
|
|
71
|
+
'/sqlite/backup', // covers /sqlite/backup and /sqlite/backups
|
|
72
|
+
'/sqlite/restore',
|
|
73
|
+
'/sqlite/export',
|
|
74
|
+
];
|
|
75
|
+
function isBulkExempt(reqPath) {
|
|
76
|
+
for (const prefix of BULK_EXEMPT_PREFIXES) {
|
|
77
|
+
if (reqPath === prefix || reqPath.startsWith(prefix + '/'))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
52
82
|
function createApiRoutes(options = {}) {
|
|
53
83
|
const router = (0, express_1.Router)();
|
|
54
84
|
const metricsCollector = (0, MetricsCollector_js_1.getMetricsCollector)();
|
|
55
85
|
const httpCfg = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http;
|
|
56
|
-
const
|
|
86
|
+
const perMinute = options.rateLimitPerMinute ?? httpCfg.rateLimitPerMinute;
|
|
57
87
|
// CORS middleware (if enabled)
|
|
58
88
|
// Security: only allow loopback origins (localhost, 127.0.0.1, [::1]) to prevent
|
|
59
89
|
// cross-origin attacks. No wildcard (*) origins; credentials are not exposed.
|
|
@@ -71,53 +101,28 @@ function createApiRoutes(options = {}) {
|
|
|
71
101
|
}
|
|
72
102
|
// JSON middleware
|
|
73
103
|
router.use(express_1.default.json());
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
// Rate limit (single tier, fixed 60s window). Disabled when perMinute === 0.
|
|
105
|
+
// Bulk routes (backup/restore/import/export/normalize) are unconditionally
|
|
106
|
+
// exempt via BULK_EXEMPT_PREFIXES — see `skip` below.
|
|
107
|
+
if (perMinute > 0) {
|
|
108
|
+
const windowMs = 60_000;
|
|
77
109
|
router.use((0, express_rate_limit_1.default)({
|
|
78
|
-
windowMs
|
|
79
|
-
max:
|
|
110
|
+
windowMs,
|
|
111
|
+
max: perMinute,
|
|
80
112
|
standardHeaders: true,
|
|
81
113
|
legacyHeaders: true,
|
|
82
114
|
validate: { ip: false },
|
|
83
|
-
skip: (req) => req.method === 'OPTIONS',
|
|
115
|
+
skip: (req) => req.method === 'OPTIONS' || isBulkExempt(req.path),
|
|
84
116
|
keyGenerator: (req) => {
|
|
85
117
|
const clientIp = req.ip || req.socket.remoteAddress;
|
|
86
118
|
return clientIp ? (0, express_rate_limit_1.ipKeyGenerator)(clientIp) : 'unknown';
|
|
87
119
|
},
|
|
88
120
|
handler: (_req, res) => {
|
|
89
|
-
const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(
|
|
121
|
+
const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(windowMs / 1000));
|
|
90
122
|
res.status(429).json({
|
|
91
123
|
error: 'Too Many Requests',
|
|
92
124
|
message: `Rate limit exceeded. Try again in ${retryAfter} second(s).`,
|
|
93
125
|
retryAfterSeconds: retryAfter,
|
|
94
|
-
tier: 'global',
|
|
95
|
-
timestamp: Date.now(),
|
|
96
|
-
});
|
|
97
|
-
},
|
|
98
|
-
}));
|
|
99
|
-
// Stricter rate limit for mutation endpoints (POST/PUT/PATCH/DELETE).
|
|
100
|
-
// Defaults to runtimeConfig httpCfg.rateLimitMutationMax (env-configurable),
|
|
101
|
-
// bounded by the global per-window cap so mutation-tier never exceeds global.
|
|
102
|
-
const mutationMax = Math.max(1, Math.min(httpCfg.rateLimitMutationMax, rateLimitOpts.max));
|
|
103
|
-
router.use((0, express_rate_limit_1.default)({
|
|
104
|
-
windowMs: rateLimitOpts.windowMs,
|
|
105
|
-
max: mutationMax,
|
|
106
|
-
standardHeaders: true,
|
|
107
|
-
legacyHeaders: false,
|
|
108
|
-
validate: { ip: false },
|
|
109
|
-
skip: (req) => req.method === 'OPTIONS' || req.method === 'GET' || req.method === 'HEAD',
|
|
110
|
-
keyGenerator: (req) => {
|
|
111
|
-
const clientIp = req.ip || req.socket.remoteAddress;
|
|
112
|
-
return `mutation:${clientIp ? (0, express_rate_limit_1.ipKeyGenerator)(clientIp) : 'unknown'}`;
|
|
113
|
-
},
|
|
114
|
-
handler: (_req, res) => {
|
|
115
|
-
const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(rateLimitOpts.windowMs / 1000));
|
|
116
|
-
res.status(429).json({
|
|
117
|
-
error: 'Too Many Requests',
|
|
118
|
-
message: `Mutation rate limit exceeded. Try again in ${retryAfter} second(s).`,
|
|
119
|
-
retryAfterSeconds: retryAfter,
|
|
120
|
-
tier: 'mutation',
|
|
121
126
|
timestamp: Date.now(),
|
|
122
127
|
});
|
|
123
128
|
},
|
|
@@ -106,19 +106,21 @@ function renderPanelMarkdownHtml(name, markdown) {
|
|
|
106
106
|
* Called once during DashboardServer construction after middleware is set up.
|
|
107
107
|
*/
|
|
108
108
|
function mountDashboardRoutes(app, ctx) {
|
|
109
|
-
// Build a per-route rate limiter using the same dashboard.http.rateLimit
|
|
110
|
-
// configuration as the /api router. Re-uses the same
|
|
111
|
-
// is consistent across the dashboard. Disabled
|
|
109
|
+
// Build a per-route rate limiter using the same dashboard.http.rateLimit
|
|
110
|
+
// configuration as the /api router. Re-uses the same per-minute cap so
|
|
111
|
+
// behavior is consistent across the dashboard. Disabled when
|
|
112
|
+
// rateLimitPerMinute === 0 (the default).
|
|
112
113
|
const httpCfg = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http;
|
|
114
|
+
const perMinute = httpCfg.rateLimitPerMinute;
|
|
113
115
|
const dashboardLimiter = (0, express_rate_limit_1.default)({
|
|
114
|
-
windowMs:
|
|
115
|
-
max:
|
|
116
|
+
windowMs: 60_000,
|
|
117
|
+
max: perMinute > 0 ? perMinute : 1,
|
|
116
118
|
standardHeaders: true,
|
|
117
119
|
legacyHeaders: false,
|
|
118
120
|
validate: { ip: false },
|
|
119
|
-
skip: () =>
|
|
121
|
+
skip: () => perMinute <= 0,
|
|
120
122
|
handler: (_req, res) => {
|
|
121
|
-
const retryAfter = Number(res.getHeader('Retry-After') ||
|
|
123
|
+
const retryAfter = Number(res.getHeader('Retry-After') || 60);
|
|
122
124
|
res.status(429).json({
|
|
123
125
|
error: 'Too Many Requests',
|
|
124
126
|
message: `Rate limit exceeded. Try again in ${retryAfter} second(s).`,
|
|
@@ -222,8 +224,8 @@ function mountDashboardRoutes(app, ctx) {
|
|
|
222
224
|
res.status(404).send('Screenshot not found');
|
|
223
225
|
}
|
|
224
226
|
});
|
|
225
|
-
// API sub-routes (mounted at /api). Rate
|
|
226
|
-
// (
|
|
227
|
+
// API sub-routes (mounted at /api). Rate limit sourced from runtimeConfig
|
|
228
|
+
// (INDEX_SERVER_RATE_LIMIT) — see ApiRoutes.createApiRoutes.
|
|
227
229
|
app.use('/api', (0, ApiRoutes_js_1.createApiRoutes)({ enableCors: ctx.enableCors }));
|
|
228
230
|
// Back-compat: legacy tests expect /tools.json at dashboard root
|
|
229
231
|
app.get('/tools.json', (_req, res) => {
|
|
@@ -112,6 +112,7 @@ export declare class McpStdioLogger {
|
|
|
112
112
|
private readonly _serverName;
|
|
113
113
|
private readonly _maxBufferSize;
|
|
114
114
|
private readonly _inferLevel;
|
|
115
|
+
private readonly _exitHandler;
|
|
115
116
|
constructor(options?: McpStdioLoggerOptions);
|
|
116
117
|
/**
|
|
117
118
|
* Start intercepting process.stderr.write.
|
|
@@ -152,6 +153,17 @@ export declare class McpStdioLogger {
|
|
|
152
153
|
* without triggering the MCP routing/buffering pipeline.
|
|
153
154
|
*/
|
|
154
155
|
writeOriginalStderr(data: string): void;
|
|
156
|
+
/**
|
|
157
|
+
* Drain the pre-handshake buffer to the ORIGINAL stderr without requiring
|
|
158
|
+
* an MCP server activation. Used when the process is about to exit before
|
|
159
|
+
* the MCP handshake completes (e.g., `--help`, `--init-cert`, or a fatal
|
|
160
|
+
* startup error). Without this, buffered diagnostic lines would be silently
|
|
161
|
+
* discarded — see issue #234.
|
|
162
|
+
*
|
|
163
|
+
* Safe to call repeatedly; a no-op when the buffer is empty. Does not change
|
|
164
|
+
* `isActive` and does not require a registered server.
|
|
165
|
+
*/
|
|
166
|
+
flushToStderr(): void;
|
|
155
167
|
/**
|
|
156
168
|
* Restore original stderr and deactivate the bridge.
|
|
157
169
|
* Useful for testing cleanup or graceful shutdown.
|
|
@@ -115,6 +115,7 @@ class McpStdioLogger {
|
|
|
115
115
|
_serverName;
|
|
116
116
|
_maxBufferSize;
|
|
117
117
|
_inferLevel;
|
|
118
|
+
_exitHandler;
|
|
118
119
|
constructor(options = {}) {
|
|
119
120
|
this._serverName = options.serverName ?? 'mcp-server';
|
|
120
121
|
this._maxBufferSize = options.maxBufferSize ?? 500;
|
|
@@ -123,6 +124,16 @@ class McpStdioLogger {
|
|
|
123
124
|
if (options.interceptImmediately !== false) {
|
|
124
125
|
this.interceptStderr();
|
|
125
126
|
}
|
|
127
|
+
// Issue #234: if the process exits before the MCP handshake completes
|
|
128
|
+
// (e.g. --help, --init-cert, fatal startup error), drain buffered stderr
|
|
129
|
+
// so the user actually sees what was logged. Inactive bridge only —
|
|
130
|
+
// once activated, the buffer is already replayed via sendLoggingMessage.
|
|
131
|
+
this._exitHandler = () => {
|
|
132
|
+
if (!this._active && this._buffer.length > 0) {
|
|
133
|
+
this.flushToStderr();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
process.on('exit', this._exitHandler);
|
|
126
137
|
}
|
|
127
138
|
// -----------------------------------------------------------------------
|
|
128
139
|
// Public API
|
|
@@ -241,6 +252,30 @@ class McpStdioLogger {
|
|
|
241
252
|
}
|
|
242
253
|
catch { /* ignore */ }
|
|
243
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Drain the pre-handshake buffer to the ORIGINAL stderr without requiring
|
|
257
|
+
* an MCP server activation. Used when the process is about to exit before
|
|
258
|
+
* the MCP handshake completes (e.g., `--help`, `--init-cert`, or a fatal
|
|
259
|
+
* startup error). Without this, buffered diagnostic lines would be silently
|
|
260
|
+
* discarded — see issue #234.
|
|
261
|
+
*
|
|
262
|
+
* Safe to call repeatedly; a no-op when the buffer is empty. Does not change
|
|
263
|
+
* `isActive` and does not require a registered server.
|
|
264
|
+
*/
|
|
265
|
+
flushToStderr() {
|
|
266
|
+
if (this._buffer.length === 0)
|
|
267
|
+
return;
|
|
268
|
+
for (const entry of this._buffer) {
|
|
269
|
+
const data = entry.data;
|
|
270
|
+
try {
|
|
271
|
+
this._originalStderrWrite(data.endsWith('\n') ? data : data + '\n');
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
/* ignore — best-effort drain */
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
this._buffer.length = 0;
|
|
278
|
+
}
|
|
244
279
|
/**
|
|
245
280
|
* Restore original stderr and deactivate the bridge.
|
|
246
281
|
* Useful for testing cleanup or graceful shutdown.
|
|
@@ -250,6 +285,10 @@ class McpStdioLogger {
|
|
|
250
285
|
this._active = false;
|
|
251
286
|
this._intercepting = false;
|
|
252
287
|
this._buffer.length = 0;
|
|
288
|
+
try {
|
|
289
|
+
process.removeListener('exit', this._exitHandler);
|
|
290
|
+
}
|
|
291
|
+
catch { /* ignore */ }
|
|
253
292
|
}
|
|
254
293
|
/**
|
|
255
294
|
* Get the number of currently buffered lines (pre-handshake).
|
|
@@ -46,8 +46,7 @@ exports.FLAG_REGISTRY = [
|
|
|
46
46
|
{ name: 'INDEX_SERVER_VISIBILITY_DIAG', category: 'tracing', description: 'Force core trace level for visibility diagnostics.', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
|
|
47
47
|
{ name: 'INDEX_SERVER_FILE_TRACE', category: 'tracing', description: 'Promote index file events to trace level (files).', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
|
|
48
48
|
// Usage & metrics
|
|
49
|
-
{ name: '
|
|
50
|
-
{ name: 'INDEX_SERVER_DISABLE_USAGE_RATE_LIMIT', category: 'usage', description: '(Deprecated) Legacy per-subsystem override to disable usage rate limit.', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
|
|
49
|
+
{ name: 'INDEX_SERVER_RATE_LIMIT', category: 'usage', description: 'Dashboard HTTP API and usage-tracking rate limit, in requests per minute. 0 (default) disables rate limiting. Bulk import/export/backup/restore routes are unconditionally exempt.', stability: 'stable', default: '0', type: 'number', since: '1.27.0' },
|
|
51
50
|
{ name: 'INDEX_SERVER_DISABLE_USAGE_CLAMP', category: 'usage', description: 'Disable initial usage count clamp logic.', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
|
|
52
51
|
{ name: 'INDEX_SERVER_USAGE_FLUSH_MS', category: 'usage', description: 'Override usage flush debounce interval.', stability: 'diagnostic', default: '75', type: 'number', since: '1.1.1' },
|
|
53
52
|
// Validation / schema
|
|
@@ -36,7 +36,6 @@ const features_1 = require("./features");
|
|
|
36
36
|
const atomicFs_1 = require("./atomicFs");
|
|
37
37
|
const classificationService_1 = require("./classificationService");
|
|
38
38
|
const ownershipService_1 = require("./ownershipService");
|
|
39
|
-
const envUtils_1 = require("../utils/envUtils");
|
|
40
39
|
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
41
40
|
const factory_1 = require("./storage/factory");
|
|
42
41
|
const migrationEngine_1 = require("./storage/migrationEngine");
|
|
@@ -202,11 +201,11 @@ function restoreLastUsedInvariant(e) {
|
|
|
202
201
|
const USAGE_RATE_LIMIT_PER_SECOND = 10; // max increments per id per second
|
|
203
202
|
const usageRateLimiter = new Map();
|
|
204
203
|
function checkUsageRateLimit(id) {
|
|
205
|
-
// Rate limiting is
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
204
|
+
// Rate limiting is opt-in. INDEX_SERVER_RATE_LIMIT=0 (default) or unset
|
|
205
|
+
// disables both the dashboard HTTP limiter and this usage limiter.
|
|
206
|
+
// Any positive integer enables both.
|
|
207
|
+
const rl = Number(process.env.INDEX_SERVER_RATE_LIMIT);
|
|
208
|
+
if (!Number.isFinite(rl) || rl <= 0)
|
|
210
209
|
return true;
|
|
211
210
|
const now = Date.now();
|
|
212
211
|
const windowStart = Math.floor(now / 1000) * 1000; // 1-second windows
|
package/dist/services/logger.js
CHANGED
|
@@ -60,6 +60,8 @@ function shouldEmit(level) {
|
|
|
60
60
|
const recordPriority = LEVEL_PRIORITY[level] ?? LEVEL_PRIORITY.INFO;
|
|
61
61
|
return recordPriority >= threshold;
|
|
62
62
|
}
|
|
63
|
+
// Single exit handler — registered once to avoid listener accumulation on re-init
|
|
64
|
+
let exitHandlerRegistered = false;
|
|
63
65
|
// Initialize file logging if INDEX_SERVER_LOG_FILE is specified
|
|
64
66
|
function initializeFileLogging() {
|
|
65
67
|
const cfg = loggingCfg();
|
|
@@ -89,6 +91,16 @@ function initializeFileLogging() {
|
|
|
89
91
|
encoding: 'utf8'
|
|
90
92
|
});
|
|
91
93
|
logFilePath = logFile;
|
|
94
|
+
// Graceful fallback: if the stream hits EPERM/EACCES, disable file logging
|
|
95
|
+
logFileHandle.on('error', (err) => {
|
|
96
|
+
process.stderr.write(`[logger] File logging error (${err.code ?? err.message}), falling back to stderr-only\n`);
|
|
97
|
+
try {
|
|
98
|
+
logFileHandle?.end();
|
|
99
|
+
}
|
|
100
|
+
catch { /* ignore */ }
|
|
101
|
+
logFileHandle = null;
|
|
102
|
+
logFilePath = undefined;
|
|
103
|
+
});
|
|
92
104
|
// NDJSON session start record
|
|
93
105
|
const sessionStart = {
|
|
94
106
|
ts: new Date().toISOString(),
|
|
@@ -98,19 +110,22 @@ function initializeFileLogging() {
|
|
|
98
110
|
};
|
|
99
111
|
const startLine = JSON.stringify(sessionStart);
|
|
100
112
|
logFileHandle.write(startLine + '\n');
|
|
101
|
-
// Cleanup on process exit
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
// Cleanup on process exit — register only once
|
|
114
|
+
if (!exitHandlerRegistered) {
|
|
115
|
+
exitHandlerRegistered = true;
|
|
116
|
+
process.on('exit', () => {
|
|
117
|
+
if (logFileHandle && !logFileHandle.destroyed) {
|
|
118
|
+
const sessionEnd = {
|
|
119
|
+
ts: new Date().toISOString(),
|
|
120
|
+
level: 'INFO',
|
|
121
|
+
msg: '[logger] Session ended',
|
|
122
|
+
pid: process.pid,
|
|
123
|
+
};
|
|
124
|
+
logFileHandle.write(JSON.stringify(sessionEnd) + '\n');
|
|
125
|
+
logFileHandle.end();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
114
129
|
// Emit NDJSON init diagnostic
|
|
115
130
|
try {
|
|
116
131
|
const stats = fs_1.default.existsSync(logFile) ? fs_1.default.statSync(logFile) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jagilber-org/index-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"mcpName": "io.github.jagilber-org/index-server",
|
|
5
5
|
"description": "MCP instruction indexing server for AI assistant governance — search, CRUD, schema validation, usage tracking, and cross-repo knowledge promotion.",
|
|
6
6
|
"publishConfig": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"manifest:generate": "node scripts/generate-manifest.mjs",
|
|
51
51
|
"contract:tools": "node scripts/generate-tools-snapshot.mjs",
|
|
52
52
|
"verify:manifest": "node scripts/verify-manifest.mjs",
|
|
53
|
+
"check:version-parity": "node scripts/check-version-parity.mjs",
|
|
53
54
|
"test": "npm run test:fast",
|
|
54
55
|
"test:all": "vitest run",
|
|
55
56
|
"test:watch": "vitest",
|