@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 +6 -0
- package/bin/token-cache-shim.mjs +162 -0
- package/package.json +1 -1
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