reflect-mcp 1.0.12 → 1.0.14
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/dist/cli.js +192 -71
- package/dist/pkcehandler.d.ts +7 -1
- package/dist/pkcehandler.js +166 -44
- package/dist/server.d.ts +9 -0
- package/dist/server.js +41 -1
- package/dist/utils.d.ts +5 -3
- package/dist/utils.js +16 -7
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -34,54 +34,52 @@ function isPortInUse(port) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Find a free port starting from startPort.
|
|
38
|
+
* Returns the first available port within maxAttempts tries,
|
|
39
|
+
* or throws if none is found.
|
|
38
40
|
*/
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log(`⚠️ Port ${port} in use by PID(s): ${pids.replace(/\n/g, ", ")}`);
|
|
45
|
-
execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: "ignore" });
|
|
46
|
-
console.log(`✅ Killed existing process(es) on port ${port}`);
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// No process found on port, or kill failed - that's fine
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Ensure the port is free, killing any existing process if needed
|
|
57
|
-
*/
|
|
58
|
-
async function ensurePortFree(port) {
|
|
59
|
-
if (await isPortInUse(port)) {
|
|
60
|
-
killProcessOnPort(port);
|
|
61
|
-
// Wait a moment for the port to be released
|
|
62
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
63
|
-
// Check again
|
|
64
|
-
if (await isPortInUse(port)) {
|
|
65
|
-
throw new Error(`Port ${port} is still in use after attempting to free it`);
|
|
41
|
+
async function findFreePort(startPort, maxAttempts = 10) {
|
|
42
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
43
|
+
const candidate = startPort + i;
|
|
44
|
+
if (!(await isPortInUse(candidate))) {
|
|
45
|
+
return candidate;
|
|
66
46
|
}
|
|
67
47
|
}
|
|
48
|
+
throw new Error(`No free port found in range ${startPort}–${startPort + maxAttempts - 1}`);
|
|
68
49
|
}
|
|
69
50
|
const REFLECT_CLIENT_ID = "55798f25d5a24efb95e4174fff3d219e";
|
|
51
|
+
const platform = os.platform();
|
|
52
|
+
// macOS LaunchAgent paths
|
|
70
53
|
const LAUNCH_AGENT_LABEL = "com.reflect-mcp";
|
|
71
54
|
const LAUNCH_AGENT_DIR = path.join(os.homedir(), "Library/LaunchAgents");
|
|
72
55
|
const LAUNCH_AGENT_PATH = path.join(LAUNCH_AGENT_DIR, `${LAUNCH_AGENT_LABEL}.plist`);
|
|
56
|
+
// Linux systemd user service paths
|
|
57
|
+
const SYSTEMD_SERVICE_NAME = "reflect-mcp";
|
|
58
|
+
const SYSTEMD_USER_DIR = path.join(os.homedir(), ".config/systemd/user");
|
|
59
|
+
const SYSTEMD_SERVICE_PATH = path.join(SYSTEMD_USER_DIR, `${SYSTEMD_SERVICE_NAME}.service`);
|
|
60
|
+
function requireSupportedPlatform() {
|
|
61
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
62
|
+
console.error(`❌ Service management is not supported on ${platform}.`);
|
|
63
|
+
console.error(" Supported platforms: macOS (darwin), Linux");
|
|
64
|
+
console.error(" You can still run the server directly: reflect-mcp [db-path]");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
73
68
|
// Get the command and arguments
|
|
74
69
|
const args = process.argv.slice(2);
|
|
75
70
|
const command = args[0];
|
|
76
71
|
// Handle commands
|
|
77
72
|
(async () => {
|
|
78
73
|
if (command === "install") {
|
|
74
|
+
requireSupportedPlatform();
|
|
79
75
|
await install(args.slice(1));
|
|
80
76
|
}
|
|
81
77
|
else if (command === "uninstall") {
|
|
78
|
+
requireSupportedPlatform();
|
|
82
79
|
uninstall();
|
|
83
80
|
}
|
|
84
81
|
else if (command === "status") {
|
|
82
|
+
requireSupportedPlatform();
|
|
85
83
|
status();
|
|
86
84
|
}
|
|
87
85
|
else if (command === "--help" || command === "-h") {
|
|
@@ -93,6 +91,9 @@ const command = args[0];
|
|
|
93
91
|
}
|
|
94
92
|
})();
|
|
95
93
|
function showHelp() {
|
|
94
|
+
const dbDefault = DEFAULT_DB_PATH
|
|
95
|
+
? `(default: ${DEFAULT_DB_PATH})`
|
|
96
|
+
: "(required on Linux — no default path)";
|
|
96
97
|
console.log(`
|
|
97
98
|
Reflect MCP Server - Connect your Reflect notes to Claude
|
|
98
99
|
|
|
@@ -104,13 +105,13 @@ Usage:
|
|
|
104
105
|
|
|
105
106
|
Arguments:
|
|
106
107
|
db-path Path to Reflect SQLite database
|
|
107
|
-
|
|
108
|
+
${dbDefault}
|
|
108
109
|
|
|
109
110
|
Options:
|
|
110
111
|
--port <port> Port to run server on (default: 3000)
|
|
111
112
|
|
|
112
113
|
Examples:
|
|
113
|
-
reflect-mcp install # Install with default db path
|
|
114
|
+
reflect-mcp install # Install with default db path (macOS)
|
|
114
115
|
reflect-mcp install ~/my/reflect/db # Install with custom db path
|
|
115
116
|
reflect-mcp uninstall # Remove auto-start
|
|
116
117
|
reflect-mcp # Run server manually
|
|
@@ -118,7 +119,7 @@ Examples:
|
|
|
118
119
|
process.exit(0);
|
|
119
120
|
}
|
|
120
121
|
async function install(installArgs) {
|
|
121
|
-
let dbPath
|
|
122
|
+
let dbPath;
|
|
122
123
|
let port = 3000;
|
|
123
124
|
// Parse install arguments
|
|
124
125
|
for (let i = 0; i < installArgs.length; i++) {
|
|
@@ -129,11 +130,47 @@ async function install(installArgs) {
|
|
|
129
130
|
dbPath = installArgs[i];
|
|
130
131
|
}
|
|
131
132
|
}
|
|
133
|
+
// On Linux there's no known default path -- require explicit db-path
|
|
134
|
+
if (!dbPath) {
|
|
135
|
+
if (platform === "linux") {
|
|
136
|
+
console.error("❌ On Linux, you must specify the database path:");
|
|
137
|
+
console.error(" reflect-mcp install /path/to/reflect.db");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
dbPath = DEFAULT_DB_PATH;
|
|
141
|
+
}
|
|
132
142
|
const expandedDbPath = expandPath(dbPath);
|
|
133
143
|
const nodePath = process.execPath;
|
|
134
144
|
const cliPath = process.argv[1];
|
|
135
145
|
console.log("📦 Installing Reflect MCP Server as auto-start service...\n");
|
|
136
|
-
|
|
146
|
+
if (platform === "darwin") {
|
|
147
|
+
installDarwin(nodePath, cliPath, expandedDbPath, port);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
installLinux(nodePath, cliPath, expandedDbPath, port);
|
|
151
|
+
}
|
|
152
|
+
console.log(`🚀 Reflect MCP Server will now auto-start on login`);
|
|
153
|
+
console.log(` Server: http://localhost:${port}`);
|
|
154
|
+
console.log(` Database: ${expandedDbPath}`);
|
|
155
|
+
if (platform === "darwin") {
|
|
156
|
+
console.log(` Logs: tail -f /tmp/reflect-mcp.log\n`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log(` Logs: journalctl --user -u ${SYSTEMD_SERVICE_NAME} -f\n`);
|
|
160
|
+
}
|
|
161
|
+
console.log(`📋 Add to Claude Desktop config (~/.config/claude/claude_desktop_config.json):`);
|
|
162
|
+
console.log(`{
|
|
163
|
+
"mcpServers": {
|
|
164
|
+
"reflect": {
|
|
165
|
+
"command": "npx",
|
|
166
|
+
"args": ["-y", "mcp-remote", "http://localhost:${port}/mcp", "--port", "4209"]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}`);
|
|
170
|
+
console.log(` Note: Make sure the port (${port}) matches the port you used when installing the server.`);
|
|
171
|
+
console.log(` Important: Add "--port", "4209" (or a different port like "4210", "4211", etc.) to avoid conflicts if you have multiple MCP clients running.`);
|
|
172
|
+
}
|
|
173
|
+
function installDarwin(nodePath, cliPath, expandedDbPath, port) {
|
|
137
174
|
if (!fs.existsSync(LAUNCH_AGENT_DIR)) {
|
|
138
175
|
fs.mkdirSync(LAUNCH_AGENT_DIR, { recursive: true });
|
|
139
176
|
}
|
|
@@ -145,10 +182,6 @@ async function install(installArgs) {
|
|
|
145
182
|
catch {
|
|
146
183
|
// Ignore errors - service might not exist yet
|
|
147
184
|
}
|
|
148
|
-
// Kill any stale processes on the port
|
|
149
|
-
killProcessOnPort(port);
|
|
150
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
151
|
-
// Create plist content
|
|
152
185
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
153
186
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
154
187
|
<plist version="1.0">
|
|
@@ -173,10 +206,8 @@ async function install(installArgs) {
|
|
|
173
206
|
<string>/tmp/reflect-mcp.log</string>
|
|
174
207
|
</dict>
|
|
175
208
|
</plist>`;
|
|
176
|
-
// Write plist file
|
|
177
209
|
fs.writeFileSync(LAUNCH_AGENT_PATH, plist);
|
|
178
210
|
console.log(`✅ Created: ${LAUNCH_AGENT_PATH}`);
|
|
179
|
-
// Load and start the service
|
|
180
211
|
try {
|
|
181
212
|
execSync(`launchctl load ${LAUNCH_AGENT_PATH}`);
|
|
182
213
|
execSync(`launchctl start ${LAUNCH_AGENT_LABEL}`);
|
|
@@ -186,40 +217,90 @@ async function install(installArgs) {
|
|
|
186
217
|
console.error("❌ Failed to start service:", error);
|
|
187
218
|
process.exit(1);
|
|
188
219
|
}
|
|
189
|
-
console.log(`🚀 Reflect MCP Server will now auto-start on login`);
|
|
190
|
-
console.log(` Server: http://localhost:${port}`);
|
|
191
|
-
console.log(` Database: ${expandedDbPath}`);
|
|
192
|
-
console.log(` Logs: tail -f /tmp/reflect-mcp.log\n`);
|
|
193
|
-
console.log(`📋 Add to Claude Desktop config (~/.config/claude/claude_desktop_config.json):`);
|
|
194
|
-
console.log(`{
|
|
195
|
-
"mcpServers": {
|
|
196
|
-
"reflect": {
|
|
197
|
-
"command": "npx",
|
|
198
|
-
"args": ["-y", "mcp-remote", "--port", "4209", "http://localhost:${port}/mcp"]
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}`);
|
|
202
|
-
console.log(` Note: Make sure the port (${port}) matches the port you used when installing the server.`);
|
|
203
|
-
console.log(` Important: Add "--port", "4209" (or a different port like "4210", "4211", etc.) to avoid conflicts if you have multiple MCP clients running.`);
|
|
204
220
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
221
|
+
function installLinux(nodePath, cliPath, expandedDbPath, port) {
|
|
222
|
+
if (!fs.existsSync(SYSTEMD_USER_DIR)) {
|
|
223
|
+
fs.mkdirSync(SYSTEMD_USER_DIR, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
// Stop existing service if running
|
|
207
226
|
try {
|
|
208
|
-
execSync(`
|
|
209
|
-
execSync(`
|
|
227
|
+
execSync(`systemctl --user stop ${SYSTEMD_SERVICE_NAME} 2>/dev/null`, { stdio: "ignore" });
|
|
228
|
+
execSync(`systemctl --user disable ${SYSTEMD_SERVICE_NAME} 2>/dev/null`, { stdio: "ignore" });
|
|
210
229
|
}
|
|
211
230
|
catch {
|
|
212
|
-
// Ignore errors
|
|
231
|
+
// Ignore errors - service might not exist yet
|
|
213
232
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
const unit = `[Unit]
|
|
234
|
+
Description=Reflect MCP Server
|
|
235
|
+
After=network.target
|
|
236
|
+
|
|
237
|
+
[Service]
|
|
238
|
+
ExecStart=${nodePath} ${cliPath} ${expandedDbPath} --port ${port}
|
|
239
|
+
Restart=always
|
|
240
|
+
StandardOutput=journal
|
|
241
|
+
StandardError=journal
|
|
242
|
+
|
|
243
|
+
[Install]
|
|
244
|
+
WantedBy=default.target
|
|
245
|
+
`;
|
|
246
|
+
fs.writeFileSync(SYSTEMD_SERVICE_PATH, unit);
|
|
247
|
+
console.log(`✅ Created: ${SYSTEMD_SERVICE_PATH}`);
|
|
248
|
+
try {
|
|
249
|
+
execSync("systemctl --user daemon-reload");
|
|
250
|
+
execSync(`systemctl --user enable --now ${SYSTEMD_SERVICE_NAME}`);
|
|
251
|
+
console.log("✅ Service installed and started!\n");
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
console.error("❌ Failed to start service:", error);
|
|
255
|
+
console.error(" Make sure systemd user services are available (systemctl --user).");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function uninstall() {
|
|
260
|
+
console.log("🗑️ Removing Reflect MCP Server auto-start service...\n");
|
|
261
|
+
if (platform === "darwin") {
|
|
262
|
+
try {
|
|
263
|
+
execSync(`launchctl stop ${LAUNCH_AGENT_LABEL} 2>/dev/null`, { stdio: "ignore" });
|
|
264
|
+
execSync(`launchctl unload ${LAUNCH_AGENT_PATH} 2>/dev/null`, { stdio: "ignore" });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Ignore errors
|
|
268
|
+
}
|
|
269
|
+
if (fs.existsSync(LAUNCH_AGENT_PATH)) {
|
|
270
|
+
fs.unlinkSync(LAUNCH_AGENT_PATH);
|
|
271
|
+
console.log(`✅ Removed: ${LAUNCH_AGENT_PATH}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
try {
|
|
276
|
+
execSync(`systemctl --user disable --now ${SYSTEMD_SERVICE_NAME} 2>/dev/null`, { stdio: "ignore" });
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Ignore errors
|
|
280
|
+
}
|
|
281
|
+
if (fs.existsSync(SYSTEMD_SERVICE_PATH)) {
|
|
282
|
+
fs.unlinkSync(SYSTEMD_SERVICE_PATH);
|
|
283
|
+
console.log(`✅ Removed: ${SYSTEMD_SERVICE_PATH}`);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
execSync("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Ignore errors
|
|
290
|
+
}
|
|
217
291
|
}
|
|
218
292
|
console.log("✅ Service uninstalled. Server will no longer auto-start.");
|
|
219
293
|
}
|
|
220
294
|
function status() {
|
|
221
295
|
console.log("📊 Reflect MCP Server Status\n");
|
|
222
|
-
|
|
296
|
+
if (platform === "darwin") {
|
|
297
|
+
statusDarwin();
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
statusLinux();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function statusDarwin() {
|
|
223
304
|
if (fs.existsSync(LAUNCH_AGENT_PATH)) {
|
|
224
305
|
console.log(`✅ Launch Agent installed: ${LAUNCH_AGENT_PATH}`);
|
|
225
306
|
}
|
|
@@ -228,7 +309,6 @@ function status() {
|
|
|
228
309
|
console.log(" Run: reflect-mcp install");
|
|
229
310
|
return;
|
|
230
311
|
}
|
|
231
|
-
// Check if service is running
|
|
232
312
|
try {
|
|
233
313
|
const result = execSync(`launchctl list | grep ${LAUNCH_AGENT_LABEL}`, { encoding: "utf-8" });
|
|
234
314
|
if (result.includes(LAUNCH_AGENT_LABEL)) {
|
|
@@ -251,33 +331,74 @@ function status() {
|
|
|
251
331
|
}
|
|
252
332
|
console.log(`\n📝 Logs: tail -f /tmp/reflect-mcp.log`);
|
|
253
333
|
}
|
|
334
|
+
function statusLinux() {
|
|
335
|
+
if (!fs.existsSync(SYSTEMD_SERVICE_PATH)) {
|
|
336
|
+
console.log("❌ Systemd service not installed");
|
|
337
|
+
console.log(" Run: reflect-mcp install /path/to/reflect.db");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
console.log(`✅ Systemd service installed: ${SYSTEMD_SERVICE_PATH}`);
|
|
341
|
+
try {
|
|
342
|
+
const result = execSync(`systemctl --user is-active ${SYSTEMD_SERVICE_NAME} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
343
|
+
if (result === "active") {
|
|
344
|
+
console.log("✅ Service running");
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
console.log(`⚠️ Service not active (state: ${result})`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// is-active exits non-zero when inactive/failed
|
|
352
|
+
try {
|
|
353
|
+
const result = execSync(`systemctl --user show ${SYSTEMD_SERVICE_NAME} --property=ActiveState --value 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
354
|
+
console.log(`❌ Service ${result || "not loaded"}`);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
console.log("❌ Service not loaded");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
console.log(`\n📝 Logs: journalctl --user -u ${SYSTEMD_SERVICE_NAME} -f`);
|
|
361
|
+
}
|
|
254
362
|
async function runServer(serverArgs) {
|
|
255
|
-
let dbPath
|
|
256
|
-
let
|
|
363
|
+
let dbPath;
|
|
364
|
+
let requestedPort = 3000;
|
|
257
365
|
for (let i = 0; i < serverArgs.length; i++) {
|
|
258
366
|
if (serverArgs[i] === "--port" && serverArgs[i + 1]) {
|
|
259
|
-
|
|
367
|
+
requestedPort = parseInt(serverArgs[++i]);
|
|
260
368
|
}
|
|
261
369
|
else if (!serverArgs[i].startsWith("--")) {
|
|
262
370
|
dbPath = serverArgs[i];
|
|
263
371
|
}
|
|
264
372
|
}
|
|
265
|
-
|
|
373
|
+
if (!dbPath) {
|
|
374
|
+
if (platform === "linux") {
|
|
375
|
+
console.error("❌ On Linux, you must specify the database path:");
|
|
376
|
+
console.error(" reflect-mcp /path/to/reflect.db");
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
dbPath = DEFAULT_DB_PATH;
|
|
380
|
+
}
|
|
381
|
+
// Find the first free port at or above the requested port.
|
|
382
|
+
// This allows multiple MCP clients to each get their own HTTP server
|
|
383
|
+
// without killing other running instances.
|
|
384
|
+
let port;
|
|
266
385
|
try {
|
|
267
|
-
await
|
|
386
|
+
port = await findFreePort(requestedPort);
|
|
268
387
|
}
|
|
269
388
|
catch (err) {
|
|
270
|
-
console.error(
|
|
389
|
+
console.error(`[reflect-mcp] ${err}`);
|
|
271
390
|
process.exit(1);
|
|
272
391
|
}
|
|
392
|
+
if (port !== requestedPort) {
|
|
393
|
+
console.error(`[reflect-mcp] Requested port ${requestedPort} is in use — using port ${port} instead`);
|
|
394
|
+
}
|
|
273
395
|
try {
|
|
274
396
|
await startReflectMCPServer({
|
|
275
397
|
clientId: REFLECT_CLIENT_ID,
|
|
276
398
|
port,
|
|
277
399
|
dbPath,
|
|
278
400
|
});
|
|
279
|
-
console.
|
|
280
|
-
console.log(`Database: ${dbPath}`);
|
|
401
|
+
console.error(`[reflect-mcp] HTTP server running on http://localhost:${port}`);
|
|
281
402
|
}
|
|
282
403
|
catch (err) {
|
|
283
404
|
console.error("Failed to start server:", err);
|
package/dist/pkcehandler.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PKCE OAuth Proxy (No Client Secret Required)
|
|
3
3
|
*
|
|
4
|
+
* Updated by Twice 🦸♂️
|
|
5
|
+
* Cloning capabilities enabled for multiple MCP clients
|
|
6
|
+
*
|
|
4
7
|
* This module provides a custom OAuth proxy that uses PKCE for authentication
|
|
5
8
|
* without requiring a client secret, suitable for public clients.
|
|
6
9
|
*/
|
|
@@ -25,6 +28,9 @@ export declare class PKCEOAuthProxy {
|
|
|
25
28
|
private tokens;
|
|
26
29
|
private recentlyExchangedCodes;
|
|
27
30
|
private cleanupInterval;
|
|
31
|
+
private tokenMutex;
|
|
32
|
+
private tokenMutexBusy;
|
|
33
|
+
private activeConnections;
|
|
28
34
|
constructor(options: PKCEOAuthProxyConfig);
|
|
29
35
|
private loadTokensFromDisk;
|
|
30
36
|
private saveTokensToDisk;
|
|
@@ -85,7 +91,7 @@ export declare class PKCEOAuthProxy {
|
|
|
85
91
|
client_name?: string;
|
|
86
92
|
redirect_uris?: string[];
|
|
87
93
|
}>;
|
|
88
|
-
loadUpstreamTokens(proxyToken: string): TokenData | null
|
|
94
|
+
loadUpstreamTokens(proxyToken: string): Promise<TokenData | null>;
|
|
89
95
|
getFirstValidToken(): TokenData | null;
|
|
90
96
|
private startCleanup;
|
|
91
97
|
destroy(): void;
|
package/dist/pkcehandler.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PKCE OAuth Proxy (No Client Secret Required)
|
|
3
3
|
*
|
|
4
|
+
* Updated by Twice 🦸♂️
|
|
5
|
+
* Cloning capabilities enabled for multiple MCP clients
|
|
6
|
+
*
|
|
4
7
|
* This module provides a custom OAuth proxy that uses PKCE for authentication
|
|
5
8
|
* without requiring a client secret, suitable for public clients.
|
|
6
9
|
*/
|
|
@@ -10,6 +13,56 @@ import * as path from "path";
|
|
|
10
13
|
import * as os from "os";
|
|
11
14
|
import { OAuthProxyError } from "fastmcp/auth";
|
|
12
15
|
// ============================================================================
|
|
16
|
+
// Write Queue - Prevents concurrent file I/O operations
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Simple in-memory write queue that batches and serializes file writes.
|
|
20
|
+
* This prevents race conditions when multiple clients trigger disk writes simultaneously.
|
|
21
|
+
*/
|
|
22
|
+
class WriteQueue {
|
|
23
|
+
queue = [];
|
|
24
|
+
isProcessing = false;
|
|
25
|
+
/**
|
|
26
|
+
* Add a write operation to the queue and wait for it to complete
|
|
27
|
+
*/
|
|
28
|
+
async add(writeOperation) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const operation = async () => {
|
|
31
|
+
try {
|
|
32
|
+
await writeOperation();
|
|
33
|
+
resolve();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
reject(error);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
this.queue.push(operation);
|
|
40
|
+
this.processQueue();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Process the queue one operation at a time
|
|
45
|
+
*/
|
|
46
|
+
async processQueue() {
|
|
47
|
+
if (this.isProcessing || this.queue.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.isProcessing = true;
|
|
51
|
+
const operation = this.queue.shift();
|
|
52
|
+
try {
|
|
53
|
+
await operation();
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
this.isProcessing = false;
|
|
57
|
+
// Small delay to batch rapid writes together
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
59
|
+
this.processQueue();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Global write queue instance shared across all PKCEProxy instances
|
|
64
|
+
const globalWriteQueue = new WriteQueue();
|
|
65
|
+
// ============================================================================
|
|
13
66
|
// PKCEOAuthProxy Class
|
|
14
67
|
// ============================================================================
|
|
15
68
|
export class PKCEOAuthProxy {
|
|
@@ -21,6 +74,12 @@ export class PKCEOAuthProxy {
|
|
|
21
74
|
// Track tokens that have been exchanged but allow brief retry window
|
|
22
75
|
recentlyExchangedCodes = new Map();
|
|
23
76
|
cleanupInterval = null;
|
|
77
|
+
// In-memory mutex to prevent race conditions during token operations
|
|
78
|
+
// This ensures only one client can exchange a code at a time
|
|
79
|
+
tokenMutex = new Map();
|
|
80
|
+
tokenMutexBusy = false;
|
|
81
|
+
// Active connections counter for debugging
|
|
82
|
+
activeConnections = 0;
|
|
24
83
|
constructor(options) {
|
|
25
84
|
this.config = {
|
|
26
85
|
baseUrl: options.baseUrl,
|
|
@@ -60,8 +119,9 @@ export class PKCEOAuthProxy {
|
|
|
60
119
|
console.warn("[PKCEProxy] Failed to load tokens from disk:", error);
|
|
61
120
|
}
|
|
62
121
|
}
|
|
63
|
-
// Save tokens to disk
|
|
64
|
-
|
|
122
|
+
// Save tokens to disk - ASYNC with write queue
|
|
123
|
+
// This prevents blocking the event loop and prevents race conditions
|
|
124
|
+
async saveTokensToDisk() {
|
|
65
125
|
try {
|
|
66
126
|
const toStore = {};
|
|
67
127
|
for (const [key, value] of this.tokens) {
|
|
@@ -71,7 +131,11 @@ export class PKCEOAuthProxy {
|
|
|
71
131
|
expiresAt: value.expiresAt.toISOString(),
|
|
72
132
|
};
|
|
73
133
|
}
|
|
74
|
-
|
|
134
|
+
// Use the global write queue to serialize this write operation
|
|
135
|
+
await globalWriteQueue.add(async () => {
|
|
136
|
+
await fs.promises.writeFile(this.config.tokenStoragePath, JSON.stringify(toStore, null, 2), "utf-8");
|
|
137
|
+
});
|
|
138
|
+
console.log(`[PKCEProxy] Saved ${toStore.length} tokens to disk`);
|
|
75
139
|
}
|
|
76
140
|
catch (error) {
|
|
77
141
|
console.error("[PKCEProxy] Failed to save tokens to disk:", error);
|
|
@@ -106,8 +170,8 @@ export class PKCEOAuthProxy {
|
|
|
106
170
|
console.warn("[PKCEProxy] Failed to load transactions from disk:", error);
|
|
107
171
|
}
|
|
108
172
|
}
|
|
109
|
-
// Save transactions to disk (survives server restarts)
|
|
110
|
-
saveTransactionsToDisk() {
|
|
173
|
+
// Save transactions to disk (survives server restarts) - ASYNC with write queue
|
|
174
|
+
async saveTransactionsToDisk() {
|
|
111
175
|
try {
|
|
112
176
|
const toStore = {};
|
|
113
177
|
for (const [key, value] of this.transactions) {
|
|
@@ -122,7 +186,11 @@ export class PKCEOAuthProxy {
|
|
|
122
186
|
expiresAt: value.expiresAt.toISOString(),
|
|
123
187
|
};
|
|
124
188
|
}
|
|
125
|
-
|
|
189
|
+
// Use the global write queue to serialize this write operation
|
|
190
|
+
await globalWriteQueue.add(async () => {
|
|
191
|
+
await fs.promises.writeFile(this.config.transactionStoragePath, JSON.stringify(toStore, null, 2), "utf-8");
|
|
192
|
+
});
|
|
193
|
+
console.log(`[PKCEProxy] Saved ${toStore.length} transactions to disk`);
|
|
126
194
|
}
|
|
127
195
|
catch (error) {
|
|
128
196
|
console.error("[PKCEProxy] Failed to save transactions to disk:", error);
|
|
@@ -179,7 +247,7 @@ export class PKCEOAuthProxy {
|
|
|
179
247
|
expiresAt: new Date(Date.now() + 600 * 1000), // 10 minutes
|
|
180
248
|
};
|
|
181
249
|
this.transactions.set(transactionId, transaction);
|
|
182
|
-
this.saveTransactionsToDisk(); // Persist to survive restarts
|
|
250
|
+
await this.saveTransactionsToDisk(); // Persist to survive restarts (async now)
|
|
183
251
|
console.log("[PKCEProxy] Created transaction:", transactionId);
|
|
184
252
|
// Build upstream authorization URL
|
|
185
253
|
const authUrl = new URL(this.config.authorizationEndpoint);
|
|
@@ -228,7 +296,7 @@ export class PKCEOAuthProxy {
|
|
|
228
296
|
}
|
|
229
297
|
if (transaction.expiresAt < new Date()) {
|
|
230
298
|
this.transactions.delete(state);
|
|
231
|
-
this.saveTransactionsToDisk();
|
|
299
|
+
await this.saveTransactionsToDisk();
|
|
232
300
|
console.error("[PKCEProxy] Transaction expired, created:", transaction.createdAt, "expired:", transaction.expiresAt);
|
|
233
301
|
return new Response(JSON.stringify({ error: "transaction_expired" }), {
|
|
234
302
|
status: 400,
|
|
@@ -265,14 +333,14 @@ export class PKCEOAuthProxy {
|
|
|
265
333
|
refreshToken: tokens.refresh_token,
|
|
266
334
|
expiresAt: new Date(Date.now() + (tokens.expires_in || 3600) * 1000),
|
|
267
335
|
});
|
|
268
|
-
this.saveTokensToDisk(); // Persist to disk
|
|
336
|
+
await this.saveTokensToDisk(); // Persist to disk (async now)
|
|
269
337
|
// Redirect back to client with our proxy token
|
|
270
338
|
const clientRedirect = new URL(transaction.clientCallbackUrl);
|
|
271
339
|
clientRedirect.searchParams.set("code", proxyToken);
|
|
272
340
|
clientRedirect.searchParams.set("state", transaction.clientState);
|
|
273
341
|
// Clean up transaction
|
|
274
342
|
this.transactions.delete(state);
|
|
275
|
-
this.saveTransactionsToDisk();
|
|
343
|
+
await this.saveTransactionsToDisk();
|
|
276
344
|
console.log("[PKCEProxy] Redirecting to client:", clientRedirect.toString());
|
|
277
345
|
return new Response(null, {
|
|
278
346
|
status: 302,
|
|
@@ -285,11 +353,12 @@ export class PKCEOAuthProxy {
|
|
|
285
353
|
if (!params.code) {
|
|
286
354
|
throw new OAuthProxyError("invalid_request", "Missing authorization code", 400);
|
|
287
355
|
}
|
|
356
|
+
console.log(`[PKCEProxy] Exchange requested for code: ${params.code.slice(0, 8)}... (connections: ${this.activeConnections})`);
|
|
288
357
|
// Check if this code was recently exchanged (retry tolerance)
|
|
289
358
|
// This allows mcp-remote to retry if the first request timed out but actually succeeded
|
|
290
359
|
const recentExchange = this.recentlyExchangedCodes.get(params.code);
|
|
291
360
|
if (recentExchange && recentExchange.expiresAt > new Date()) {
|
|
292
|
-
console.log(
|
|
361
|
+
console.log(`[PKCEProxy] Returning cached token for retry of code: ${params.code.slice(0, 8)}...`);
|
|
293
362
|
const tokenData = this.tokens.get(recentExchange.accessToken);
|
|
294
363
|
if (tokenData) {
|
|
295
364
|
const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
|
|
@@ -300,33 +369,83 @@ export class PKCEOAuthProxy {
|
|
|
300
369
|
};
|
|
301
370
|
}
|
|
302
371
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
// Note: Not returning refresh_token since Reflect doesn't support refresh_token grant
|
|
328
|
-
// This tells the MCP client to re-authenticate via OAuth when the token expires
|
|
372
|
+
// Acquire mutex for this specific code to prevent race conditions
|
|
373
|
+
// This ensures only ONE client can exchange a given code at a time
|
|
374
|
+
let releaseMutex;
|
|
375
|
+
const acquireMutex = async () => {
|
|
376
|
+
const codeKey = `code_${params.code}`;
|
|
377
|
+
while (this.tokenMutex.has(codeKey)) {
|
|
378
|
+
// Wait for the current exchange to complete
|
|
379
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
380
|
+
}
|
|
381
|
+
// Create a pending promise to indicate we're acquiring the lock
|
|
382
|
+
const pending = new Promise(resolve => {
|
|
383
|
+
releaseMutex = resolve;
|
|
384
|
+
this.tokenMutex.set(codeKey, Promise.resolve());
|
|
385
|
+
});
|
|
386
|
+
// Check if we got the lock before another concurrent request took it
|
|
387
|
+
if (this.tokenMutex.get(codeKey) !== pending) {
|
|
388
|
+
this.tokenMutex.delete(codeKey);
|
|
389
|
+
return acquireMutex(); // Try again
|
|
390
|
+
}
|
|
391
|
+
await pending;
|
|
392
|
+
};
|
|
393
|
+
const releaseMutexForCode = (code) => {
|
|
394
|
+
const codeKey = `code_${code}`;
|
|
395
|
+
this.tokenMutex.delete(codeKey);
|
|
329
396
|
};
|
|
397
|
+
try {
|
|
398
|
+
await acquireMutex();
|
|
399
|
+
// Check again after acquiring mutex - another request might have already processed this code
|
|
400
|
+
const cachedExchange = this.recentlyExchangedCodes.get(params.code);
|
|
401
|
+
if (cachedExchange && cachedExchange.expiresAt > new Date()) {
|
|
402
|
+
const cachedTokenData = this.tokens.get(cachedExchange.accessToken);
|
|
403
|
+
if (cachedTokenData) {
|
|
404
|
+
const expiresIn = Math.floor((cachedTokenData.expiresAt.getTime() - Date.now()) / 1000);
|
|
405
|
+
return {
|
|
406
|
+
access_token: cachedExchange.accessToken,
|
|
407
|
+
token_type: "Bearer",
|
|
408
|
+
expires_in: expiresIn > 0 ? expiresIn : 3600,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const tokenData = this.tokens.get(params.code);
|
|
413
|
+
if (!tokenData) {
|
|
414
|
+
console.error(`[PKCEProxy] Token not found for code: ${params.code}`);
|
|
415
|
+
console.error(`[PKCEProxy] Available tokens:`, Array.from(this.tokens.keys()).map(k => k.slice(0, 8) + "..."));
|
|
416
|
+
console.error(`[PKCEProxy] Recently exchanged codes:`, Array.from(this.recentlyExchangedCodes.keys()).map(k => k.slice(0, 8) + "..."));
|
|
417
|
+
throw new OAuthProxyError("invalid_grant", "Invalid or expired authorization code", 400);
|
|
418
|
+
}
|
|
419
|
+
// Remove the code but keep track of it for retry tolerance (30 second window)
|
|
420
|
+
// Mark as exchanged BEFORE saving to disk to prevent race conditions
|
|
421
|
+
this.tokens.delete(params.code);
|
|
422
|
+
// Generate a new access token for the client
|
|
423
|
+
const accessToken = this.generateId();
|
|
424
|
+
this.tokens.set(accessToken, tokenData);
|
|
425
|
+
// Store the exchange for retry tolerance (30 seconds) - mark as exchanged
|
|
426
|
+
this.recentlyExchangedCodes.set(params.code, {
|
|
427
|
+
accessToken,
|
|
428
|
+
expiresAt: new Date(Date.now() + 30 * 1000),
|
|
429
|
+
});
|
|
430
|
+
// Now save to disk - this is the only write operation during the critical section
|
|
431
|
+
await this.saveTokensToDisk();
|
|
432
|
+
const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
|
|
433
|
+
console.log(`[PKCEProxy] Issued access token (expires in ${expiresIn}s) for code: ${params.code.slice(0, 8)}...`);
|
|
434
|
+
return {
|
|
435
|
+
access_token: accessToken,
|
|
436
|
+
token_type: "Bearer",
|
|
437
|
+
expires_in: expiresIn > 0 ? expiresIn : 3600,
|
|
438
|
+
// Note: Not returning refresh_token since Reflect doesn't support refresh_token grant
|
|
439
|
+
// This tells the MCP client to re-authenticate via OAuth when the token expires
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
// Release the mutex
|
|
447
|
+
releaseMutexForCode(params.code);
|
|
448
|
+
}
|
|
330
449
|
}
|
|
331
450
|
// Handle refresh token exchange
|
|
332
451
|
// Note: Reflect's API doesn't support standard refresh_token grant
|
|
@@ -349,7 +468,7 @@ export class PKCEOAuthProxy {
|
|
|
349
468
|
};
|
|
350
469
|
}
|
|
351
470
|
// Load upstream tokens for a given proxy token
|
|
352
|
-
loadUpstreamTokens(proxyToken) {
|
|
471
|
+
async loadUpstreamTokens(proxyToken) {
|
|
353
472
|
const data = this.tokens.get(proxyToken);
|
|
354
473
|
if (!data) {
|
|
355
474
|
console.warn("[PKCEProxy] Token not found:", proxyToken.slice(0, 8) + "...");
|
|
@@ -360,7 +479,7 @@ export class PKCEOAuthProxy {
|
|
|
360
479
|
if (data.expiresAt < now) {
|
|
361
480
|
console.warn("[PKCEProxy] Token expired:", proxyToken.slice(0, 8) + "...", "expired at:", data.expiresAt, "now:", now);
|
|
362
481
|
this.tokens.delete(proxyToken);
|
|
363
|
-
this.saveTokensToDisk();
|
|
482
|
+
await this.saveTokensToDisk();
|
|
364
483
|
return null;
|
|
365
484
|
}
|
|
366
485
|
const timeRemaining = Math.floor((data.expiresAt.getTime() - now.getTime()) / 1000);
|
|
@@ -380,8 +499,8 @@ export class PKCEOAuthProxy {
|
|
|
380
499
|
return null;
|
|
381
500
|
}
|
|
382
501
|
// Cleanup expired transactions, tokens, and retry cache
|
|
383
|
-
startCleanup() {
|
|
384
|
-
|
|
502
|
+
async startCleanup() {
|
|
503
|
+
const cleanup = async () => {
|
|
385
504
|
const now = new Date();
|
|
386
505
|
let tokensChanged = false;
|
|
387
506
|
let transactionsChanged = false;
|
|
@@ -405,12 +524,15 @@ export class PKCEOAuthProxy {
|
|
|
405
524
|
}
|
|
406
525
|
}
|
|
407
526
|
if (tokensChanged) {
|
|
408
|
-
this.saveTokensToDisk();
|
|
527
|
+
await this.saveTokensToDisk();
|
|
409
528
|
}
|
|
410
529
|
if (transactionsChanged) {
|
|
411
|
-
this.saveTransactionsToDisk();
|
|
530
|
+
await this.saveTransactionsToDisk();
|
|
412
531
|
}
|
|
413
|
-
}
|
|
532
|
+
};
|
|
533
|
+
this.cleanupInterval = setInterval(cleanup, 60000); // Every minute
|
|
534
|
+
// Run cleanup immediately on startup
|
|
535
|
+
await cleanup();
|
|
414
536
|
}
|
|
415
537
|
destroy() {
|
|
416
538
|
if (this.cleanupInterval) {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reflect MCP Server Factory
|
|
3
3
|
*
|
|
4
|
+
* Updated by Twice 🦸♂️
|
|
5
|
+
* Now handling multiple concurrent clients!
|
|
6
|
+
*
|
|
4
7
|
* Creates and configures the FastMCP server with PKCE OAuth
|
|
5
8
|
*/
|
|
6
9
|
export interface ServerConfig {
|
|
@@ -9,5 +12,11 @@ export interface ServerConfig {
|
|
|
9
12
|
dbPath?: string;
|
|
10
13
|
}
|
|
11
14
|
export declare function startReflectMCPServer(config: ServerConfig): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Start the Reflect MCP server in stdio mode.
|
|
17
|
+
* Used when an HTTP server is already running on the port (e.g. a second MCP client).
|
|
18
|
+
* Reads the cached OAuth token from disk instead of running the full OAuth flow.
|
|
19
|
+
*/
|
|
20
|
+
export declare function startReflectMCPServerStdio(config: ServerConfig): Promise<void>;
|
|
12
21
|
export { PKCEOAuthProxy } from "./pkcehandler.js";
|
|
13
22
|
export * from "./utils.js";
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reflect MCP Server Factory
|
|
3
3
|
*
|
|
4
|
+
* Updated by Twice 🦸♂️
|
|
5
|
+
* Now handling multiple concurrent clients!
|
|
6
|
+
*
|
|
4
7
|
* Creates and configures the FastMCP server with PKCE OAuth
|
|
5
8
|
*/
|
|
6
9
|
import { FastMCP } from "fastmcp";
|
|
@@ -44,7 +47,7 @@ export async function startReflectMCPServer(config) {
|
|
|
44
47
|
}
|
|
45
48
|
const token = authHeader.slice(7);
|
|
46
49
|
try {
|
|
47
|
-
const tokenData = pkceProxy.loadUpstreamTokens(token);
|
|
50
|
+
const tokenData = await pkceProxy.loadUpstreamTokens(token);
|
|
48
51
|
if (!tokenData) {
|
|
49
52
|
console.warn("[Auth] Token validation failed for:", token.slice(0, 8) + "... - triggering 401");
|
|
50
53
|
// Throw Response to trigger re-authentication (per FastMCP docs)
|
|
@@ -85,6 +88,43 @@ export async function startReflectMCPServer(config) {
|
|
|
85
88
|
transportType: "httpStream",
|
|
86
89
|
});
|
|
87
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Start the Reflect MCP server in stdio mode.
|
|
93
|
+
* Used when an HTTP server is already running on the port (e.g. a second MCP client).
|
|
94
|
+
* Reads the cached OAuth token from disk instead of running the full OAuth flow.
|
|
95
|
+
*/
|
|
96
|
+
export async function startReflectMCPServerStdio(config) {
|
|
97
|
+
const port = config.port || 3000;
|
|
98
|
+
const baseUrl = `http://localhost:${port}`;
|
|
99
|
+
// Instantiate proxy only to read tokens from disk — no HTTP server needed
|
|
100
|
+
const pkceProxy = new PKCEOAuthProxy({
|
|
101
|
+
baseUrl,
|
|
102
|
+
clientId: config.clientId,
|
|
103
|
+
authorizationEndpoint: "https://reflect.app/oauth",
|
|
104
|
+
tokenEndpoint: "https://reflect.app/api/oauth/token",
|
|
105
|
+
scopes: ["read:graph", "write:graph"],
|
|
106
|
+
});
|
|
107
|
+
const server = new FastMCP({
|
|
108
|
+
name: "Reflect MCP Server",
|
|
109
|
+
// For stdio, FastMCP calls authenticate(undefined). We load the token from disk.
|
|
110
|
+
authenticate: async (_request) => {
|
|
111
|
+
const tokenData = pkceProxy.getFirstValidToken();
|
|
112
|
+
if (!tokenData) {
|
|
113
|
+
console.error("[Auth] No valid token on disk. Connect via HTTP mode first to complete OAuth.");
|
|
114
|
+
throw new Error("No valid token. Please authenticate via HTTP mode first.");
|
|
115
|
+
}
|
|
116
|
+
const expiresIn = Math.floor((tokenData.expiresAt.getTime() - Date.now()) / 1000);
|
|
117
|
+
console.error("[Auth] Stdio mode: token loaded from disk, expires in:", expiresIn, "seconds");
|
|
118
|
+
return {
|
|
119
|
+
accessToken: tokenData.accessToken,
|
|
120
|
+
expiresIn,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
version: "1.0.0",
|
|
124
|
+
});
|
|
125
|
+
registerTools(server, config.dbPath);
|
|
126
|
+
await server.start({ transportType: "stdio" });
|
|
127
|
+
}
|
|
88
128
|
// Also export for programmatic use
|
|
89
129
|
export { PKCEOAuthProxy } from "./pkcehandler.js";
|
|
90
130
|
export * from "./utils.js";
|
package/dist/utils.d.ts
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function expandPath(filePath: string): string;
|
|
8
8
|
/**
|
|
9
|
-
* Searches for the Reflect local database file
|
|
10
|
-
*
|
|
9
|
+
* Searches for the Reflect local database file.
|
|
10
|
+
* Only works on macOS where the Reflect app path is known.
|
|
11
|
+
* Returns the first valid database path found, or null if not found.
|
|
11
12
|
*/
|
|
12
13
|
export declare function findLocalDatabase(): string | null;
|
|
13
14
|
/**
|
|
14
|
-
* Gets the default database path, searching for it if not provided
|
|
15
|
+
* Gets the default database path, searching for it if not provided.
|
|
16
|
+
* Returns empty string on non-macOS platforms where no default is known.
|
|
15
17
|
*/
|
|
16
18
|
export declare function getDefaultDbPath(): string;
|
|
17
19
|
export declare const DEFAULT_DB_PATH: string;
|
package/dist/utils.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as os from "os";
|
|
6
6
|
import * as fs from "fs";
|
|
7
|
-
// Base path for Reflect local database
|
|
8
|
-
const REFLECT_BASE_PATH =
|
|
7
|
+
// Base path for Reflect local database (macOS only; no known Linux path)
|
|
8
|
+
const REFLECT_BASE_PATH = os.platform() === "darwin"
|
|
9
|
+
? "~/Library/Application Support/Reflect/File System"
|
|
10
|
+
: null;
|
|
9
11
|
/**
|
|
10
12
|
* Expands ~ to the user's home directory
|
|
11
13
|
*/
|
|
@@ -16,10 +18,14 @@ export function expandPath(filePath) {
|
|
|
16
18
|
return filePath;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
|
-
* Searches for the Reflect local database file
|
|
20
|
-
*
|
|
21
|
+
* Searches for the Reflect local database file.
|
|
22
|
+
* Only works on macOS where the Reflect app path is known.
|
|
23
|
+
* Returns the first valid database path found, or null if not found.
|
|
21
24
|
*/
|
|
22
25
|
export function findLocalDatabase() {
|
|
26
|
+
if (!REFLECT_BASE_PATH) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
23
29
|
const basePath = expandPath(REFLECT_BASE_PATH);
|
|
24
30
|
if (!fs.existsSync(basePath)) {
|
|
25
31
|
return null;
|
|
@@ -69,15 +75,18 @@ export function findLocalDatabase() {
|
|
|
69
75
|
return null;
|
|
70
76
|
}
|
|
71
77
|
/**
|
|
72
|
-
* Gets the default database path, searching for it if not provided
|
|
78
|
+
* Gets the default database path, searching for it if not provided.
|
|
79
|
+
* Returns empty string on non-macOS platforms where no default is known.
|
|
73
80
|
*/
|
|
74
81
|
export function getDefaultDbPath() {
|
|
75
82
|
const found = findLocalDatabase();
|
|
76
83
|
if (found) {
|
|
77
84
|
return found;
|
|
78
85
|
}
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
if (os.platform() === "darwin") {
|
|
87
|
+
return expandPath("~/Library/Application Support/Reflect/File System/000/t/00/00000000");
|
|
88
|
+
}
|
|
89
|
+
return "";
|
|
81
90
|
}
|
|
82
91
|
// For backwards compatibility
|
|
83
92
|
export const DEFAULT_DB_PATH = getDefaultDbPath();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reflect-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "MCP server for Reflect Notes - connect your notes to Claude Desktop. Just run: npx reflect-mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
40
40
|
"better-sqlite3": "^11.10.0",
|
|
41
41
|
"fastmcp": "^3.25.4",
|
|
42
|
-
"reflect-mcp": "^1.0.
|
|
42
|
+
"reflect-mcp": "^1.0.12",
|
|
43
43
|
"zod": "^4.1.13"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|