@stealthmole/mcp 0.1.11 → 0.1.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.
package/bin/cli.mjs CHANGED
@@ -7,6 +7,7 @@ import { createServer } from "net";
7
7
  import { readFileSync } from "fs";
8
8
 
9
9
  import { ensureClaudeDesktopConfig } from "./claude-desktop-config.mjs";
10
+ import { migrateTokenCache } from "./token-cache-shim.mjs";
10
11
 
11
12
  // Read current package version for pinned auto-registration
12
13
  const PKG_VERSION = JSON.parse(
@@ -223,6 +224,11 @@ process.argv.push("--host", CALLBACK_HOST);
223
224
  process.argv.push("--static-oauth-client-info", CLIENT_INFO);
224
225
  process.argv.push("--static-oauth-client-metadata", CLIENT_METADATA);
225
226
 
227
+ // Carry a login saved by an older mcp-remote into the store the current one
228
+ // reads. Must run before the proxy starts, otherwise it asks for a browser
229
+ // sign-in the customer has already completed once.
230
+ migrateTokenCache({ serverUrl: SERVER_URL, debug: DEBUG });
231
+
226
232
  // Suppress mcp-remote startup logs unless --debug is set.
227
233
  // Uses mcp-remote's built-in --silent flag (safer than overriding console.error).
228
234
  if (!DEBUG) {
@@ -0,0 +1,162 @@
1
+ // Carry a saved OAuth login over to mcp-remote's renamed token store.
2
+ //
3
+ // mcp-remote 0.15 moved its token store from `~/.mcp-auth/mcp-remote-<version>/`
4
+ // to `~/.mcp-auth/mcp-remote-v<CONFIG_STORE_VERSION>/`. Tokens written by the
5
+ // version we shipped before (0.1.38) are therefore in a directory the current
6
+ // one never looks at, which pushes every upgrading customer into a fresh browser
7
+ // login. Customers who do not complete that login get no error — the proxy waits
8
+ // for authorization indefinitely and Claude Desktop simply shows no tools.
9
+ //
10
+ // The store filename is `md5(serverUrl)_tokens.json` in both versions and the
11
+ // added fields (`issuer`, `expires_at`, `requested_scope`) are optional, so
12
+ // copying the old file across is enough: mcp-remote gets a 401 on the stale
13
+ // access token and silently refreshes it with the Keycloak offline token.
14
+ //
15
+ // TEMPORARY. This couples to mcp-remote's private store layout. Delete it once
16
+ // either is true:
17
+ // - mcp-remote bumps CONFIG_STORE_VERSION past 1, making the target dead, or
18
+ // - no customers remain on a version older than 0.1.12.
19
+ //
20
+ // NOTE: In MCP (Model Context Protocol), stdout is reserved for JSON-RPC messages.
21
+ // All user-facing messages MUST use console.error (stderr), not console.log (stdout).
22
+
23
+ import { readdirSync, statSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
24
+ import { join, dirname } from "path";
25
+ import { homedir } from "os";
26
+ import { createHash } from "crypto";
27
+
28
+ const LOG_PREFIX = "[stealthmole-mcp]";
29
+
30
+ /**
31
+ * Store directory mcp-remote 0.15 reads.
32
+ *
33
+ * Mirrors `mcp-remote-v${CONFIG_STORE_VERSION}` in its `lib/mcp-auth-config.ts`.
34
+ */
35
+ export const CONFIG_STORE_DIR = "mcp-remote-v1";
36
+
37
+ /**
38
+ * Store directories named after an mcp-remote release, e.g. `mcp-remote-0.1.37`.
39
+ *
40
+ * Deliberately anchored on a digit: a `mcp-remote-v2` directory is a *newer*
41
+ * layout, and copying out of it would move tokens into a store that has been
42
+ * retired.
43
+ */
44
+ const LEGACY_STORE_DIR = /^mcp-remote-\d/;
45
+
46
+ /**
47
+ * Resolve the base directory holding mcp-remote's token stores.
48
+ *
49
+ * @param {Record<string, string|undefined>} [env] environment variables
50
+ * @param {string} [home] home directory; injectable so tests can stay hermetic
51
+ * @returns {string} absolute path
52
+ */
53
+ export function resolveBaseDir(env = process.env, home = homedir()) {
54
+ return env.MCP_REMOTE_CONFIG_DIR || join(home, ".mcp-auth");
55
+ }
56
+
57
+ /**
58
+ * Find the legacy store holding the most recently written copy of `tokensFile`.
59
+ *
60
+ * Ordering is by the token file's own mtime rather than the directory's: the
61
+ * directory is touched by lock and verifier files from flows that never
62
+ * completed, so only the token file identifies where the customer last
63
+ * successfully signed in.
64
+ *
65
+ * @param {string} baseDir
66
+ * @param {string} tokensFile
67
+ * @returns {string|null} absolute path to the newest match
68
+ */
69
+ function findLegacyTokens(baseDir, tokensFile) {
70
+ let entries;
71
+ try {
72
+ entries = readdirSync(baseDir, { withFileTypes: true });
73
+ } catch (err) {
74
+ if (err.code === "ENOENT") {
75
+ // mcp-remote has never run on this machine — nothing to carry over
76
+ return null;
77
+ }
78
+ throw err;
79
+ }
80
+
81
+ let newest = null;
82
+ let newestMtime = -Infinity;
83
+
84
+ for (const entry of entries) {
85
+ if (!entry.isDirectory() || !LEGACY_STORE_DIR.test(entry.name)) {
86
+ continue;
87
+ }
88
+
89
+ const candidate = join(baseDir, entry.name, tokensFile);
90
+ let mtimeMs;
91
+ try {
92
+ mtimeMs = statSync(candidate).mtimeMs;
93
+ } catch {
94
+ // No token file for this server in that store
95
+ continue;
96
+ }
97
+
98
+ if (mtimeMs > newestMtime) {
99
+ newest = candidate;
100
+ newestMtime = mtimeMs;
101
+ }
102
+ }
103
+
104
+ return newest;
105
+ }
106
+
107
+ /**
108
+ * Copy a saved login into the token store the current mcp-remote reads.
109
+ *
110
+ * Non-destructive in every direction: an existing token file is never
111
+ * overwritten, the legacy copy is never deleted, and a hash mismatch or an
112
+ * unreadable store leaves the normal browser sign-in as the fallback.
113
+ *
114
+ * Outcomes:
115
+ * - `"copied"` — a legacy login was carried over
116
+ * - `"present"` — the current store already has a token file
117
+ * - `"none"` — no legacy login found for this server
118
+ * - `"failed"` — the store could not be read or written; non-fatal
119
+ *
120
+ * @param {object} options
121
+ * @param {string} options.serverUrl MCP server URL the tokens belong to
122
+ * @param {string} [options.baseDir] override the resolved store base directory
123
+ * @param {boolean} [options.debug] include the error message on failure
124
+ * @returns {"copied"|"present"|"none"|"failed"}
125
+ */
126
+ export function migrateTokenCache({ serverUrl, baseDir = resolveBaseDir(), debug = false }) {
127
+ try {
128
+ const serverUrlHash = createHash("md5").update(serverUrl).digest("hex");
129
+ const tokensFile = `${serverUrlHash}_tokens.json`;
130
+ const target = join(baseDir, CONFIG_STORE_DIR, tokensFile);
131
+
132
+ if (existsSync(target)) {
133
+ return "present";
134
+ }
135
+
136
+ const source = findLegacyTokens(baseDir, tokensFile);
137
+ if (!source) {
138
+ return "none";
139
+ }
140
+
141
+ // mcp-remote creates this itself, but only after the point where it would
142
+ // already have asked for a browser login — upgrades straight from 0.1.8
143
+ // reach here with no store at all.
144
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
145
+
146
+ // Read-then-write rather than copyFile: the source mode is whatever the
147
+ // older version left behind, and a refresh token must stay owner-only.
148
+ writeFileSync(target, readFileSync(source), { mode: 0o600 });
149
+
150
+ // Logged unconditionally, not just under --debug: support needs to be able
151
+ // to tell "the shim ran" apart from "the shim found nothing".
152
+ console.error(`${LOG_PREFIX} ✔ Restored your saved sign-in from a previous version.`);
153
+ return "copied";
154
+ } catch (err) {
155
+ // Non-fatal: the customer signs in through the browser as before
156
+ if (debug) {
157
+ console.error(`${LOG_PREFIX} ⚠ Could not carry over the saved sign-in: ${err.message}`);
158
+ console.error(err.stack);
159
+ }
160
+ return "failed";
161
+ }
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stealthmole/mcp",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "StealthMole Threat Intelligence MCP client - connect Claude Desktop, Cursor, and other MCP clients to StealthMole API",
5
5
  "bin": {
6
6
  "stealthmole-mcp": "bin/cli.mjs"