aiquila-mcp 0.3.11 → 0.3.12

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.
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
4
- import { join } from 'node:path';
4
+ import { join, dirname } from 'node:path';
5
5
  import { logger } from '../logger.js';
6
6
  const CODE_TTL_MS = 5 * 60 * 1000; // 5 minutes
7
7
  const REFRESH_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -9,6 +9,39 @@ const DEFAULT_STATE_DIR = '/app/state';
9
9
  function stateDir() {
10
10
  return (process.env.MCP_AUTH_STATE_DIR ?? DEFAULT_STATE_DIR).replace(/\/+$/, '');
11
11
  }
12
+ // True once we've emitted the loud "state dir not writable" warning, so the
13
+ // startup probe and the first failed persist warn at most once between them —
14
+ // subsequent persist failures drop to debug to avoid flooding the logs.
15
+ let warnedUnwritable = false;
16
+ /**
17
+ * Operator-facing remediation for an unwritable state directory. The fix is to
18
+ * recreate the (root-owned) Docker named volume so a fresh one inherits the
19
+ * image's node ownership — `docker compose exec ... chown` cannot be used here
20
+ * because the container would otherwise be in a crash loop. See discussion #342.
21
+ */
22
+ export function stateUnwritableMessage(dir, code) {
23
+ return (`State directory ${dir} is not writable${code ? ` (${code})` : ''} — ` +
24
+ `OAuth tokens will NOT persist; clients must re-authenticate after every restart. ` +
25
+ `To restore persistence, recreate the state volume (this clears existing tokens; ` +
26
+ `clients re-authenticate once):\n` +
27
+ ` docker compose down mcp && docker volume rm <project>_mcp_state && docker compose up -d mcp\n` +
28
+ `See https://github.com/elgorro/aiquila/discussions/342`);
29
+ }
30
+ /**
31
+ * Marks the unwritable-state warning as already emitted (called by the startup
32
+ * probe in the HTTP transport) so the first failed persist does not warn twice.
33
+ */
34
+ export function markStateUnwritableWarned() {
35
+ warnedUnwritable = true;
36
+ }
37
+ function warnPersistFailed(dir, code, err) {
38
+ if (warnedUnwritable) {
39
+ logger.debug({ dir, code, err }, '[state] persist failed — state dir not writable');
40
+ return;
41
+ }
42
+ warnedUnwritable = true;
43
+ logger.warn({ dir, code }, stateUnwritableMessage(dir, code));
44
+ }
12
45
  function ensureDir(dir) {
13
46
  try {
14
47
  mkdirSync(dir, { recursive: true });
@@ -49,7 +82,10 @@ function saveJson(filePath, data) {
49
82
  catch {
50
83
  // ignore cleanup failure
51
84
  }
52
- throw err;
85
+ // Graceful degradation: a persist failure must never crash a request or the
86
+ // process. The server keeps running with in-memory state (tokens just won't
87
+ // survive a restart) and warns the operator how to fix it. See discussion #342.
88
+ warnPersistFailed(dirname(filePath), err.code, err);
53
89
  }
54
90
  }
55
91
  export class StateDirNotWritableError extends Error {
@@ -7,7 +7,7 @@ import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
7
7
  import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
8
8
  import { createServer, SERVER_VERSION } from '../server.js';
9
9
  import { NextcloudOAuthProvider } from '../auth/provider.js';
10
- import { probeStateDir, StateDirNotWritableError } from '../auth/store.js';
10
+ import { probeStateDir, StateDirNotWritableError, stateUnwritableMessage, markStateUnwritableWarned, } from '../auth/store.js';
11
11
  import { loginHandler } from '../auth/login.js';
12
12
  import { logger } from '../logger.js';
13
13
  import { fetchStatus } from '../client/ocs.js';
@@ -182,11 +182,16 @@ export async function startHttp() {
182
182
  }
183
183
  catch (err) {
184
184
  if (err instanceof StateDirNotWritableError) {
185
- logger.fatal({ dir: err.dir, code: err.cause.code }, `[startup] State directory is not writable — refresh tokens cannot be persisted. ` +
186
- `Fix volume ownership and restart:\n docker compose exec -u 0 mcp chown -R node:node ${err.dir}\n docker compose restart mcp`);
187
- process.exit(1);
185
+ // Degrade gracefully rather than crash-loop: the server still serves
186
+ // requests, it just can't persist OAuth tokens until the operator fixes
187
+ // the volume. Crashing here was un-fixable under `restart: unless-stopped`
188
+ // because `docker compose exec` needs a running container (discussion #342).
189
+ logger.warn({ dir: err.dir, code: err.cause.code }, stateUnwritableMessage(err.dir, err.cause.code));
190
+ markStateUnwritableWarned();
191
+ }
192
+ else {
193
+ throw err;
188
194
  }
189
- throw err;
190
195
  }
191
196
  const provider = new NextcloudOAuthProvider();
192
197
  // When gated dynamic registration is desired, require a bearer token on POST /register.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiquila-mcp",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "AIquila - MCP server for Nextcloud integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",