@loopops/mcp-server 2.0.0 → 2.0.2
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/api-client.js +66 -23
- package/dist/tools/eng.js +2 -20
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* updater helper, and also keep the latest rotated token in memory
|
|
21
21
|
* in case Claude Desktop doesn't restart before next use.
|
|
22
22
|
*/
|
|
23
|
-
import { homedir } from "node:os";
|
|
23
|
+
import { homedir, platform } from "node:os";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
26
26
|
const apiUrl = process.env.API_URL;
|
|
@@ -153,34 +153,77 @@ function isRetryable(err) {
|
|
|
153
153
|
return false;
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
156
|
+
* OS-specific location Claude Desktop reads its config from. We have
|
|
157
|
+
* to write here in addition to ~/.mcp.json, because Claude Desktop
|
|
158
|
+
* does NOT read ~/.mcp.json on macOS (it reads Application Support
|
|
159
|
+
* instead). If we only update ~/.mcp.json + ~/.claude/settings.json,
|
|
160
|
+
* Claude Desktop falls behind after every rotation and breaks with
|
|
161
|
+
* "invalid_grant" on its next cold start.
|
|
162
|
+
*/
|
|
163
|
+
function claudeDesktopConfigPath() {
|
|
164
|
+
const home = homedir();
|
|
165
|
+
switch (platform()) {
|
|
166
|
+
case "darwin":
|
|
167
|
+
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
168
|
+
case "win32": {
|
|
169
|
+
const appdata = process.env.APPDATA;
|
|
170
|
+
return appdata
|
|
171
|
+
? join(appdata, "Claude", "claude_desktop_config.json")
|
|
172
|
+
: null;
|
|
173
|
+
}
|
|
174
|
+
case "linux":
|
|
175
|
+
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
176
|
+
default:
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Persist a rotated refresh token across every config location we know
|
|
182
|
+
* users store MCP settings in:
|
|
183
|
+
* - ~/.mcp.json (CLI canonical, also legacy Claude Desktop path)
|
|
184
|
+
* - ~/.claude/settings.json (Claude Code)
|
|
185
|
+
* - OS-specific Claude Desktop config (macOS/Windows/Linux)
|
|
186
|
+
*
|
|
187
|
+
* We update whichever files have the loop-operations stanza. Missing
|
|
188
|
+
* files are skipped silently. This mirrors @loopops/mcp-cli's
|
|
189
|
+
* `updateRefreshToken` helper — keep the two in sync.
|
|
190
|
+
*
|
|
191
|
+
* Best-effort: if all writes fail, we still keep the new token
|
|
192
|
+
* in-memory so the current subprocess keeps working. The next cold
|
|
193
|
+
* spawn is the one that breaks.
|
|
160
194
|
*/
|
|
161
195
|
function persistRotatedRefreshToken(newToken) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return;
|
|
171
|
-
stanza.env.OKTA_REFRESH_TOKEN = newToken;
|
|
172
|
-
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
196
|
+
const paths = [
|
|
197
|
+
join(homedir(), ".mcp.json"),
|
|
198
|
+
join(homedir(), ".claude", "settings.json"),
|
|
199
|
+
];
|
|
200
|
+
const desktop = claudeDesktopConfigPath();
|
|
201
|
+
if (desktop)
|
|
202
|
+
paths.push(desktop);
|
|
203
|
+
for (const path of paths) {
|
|
173
204
|
try {
|
|
174
|
-
|
|
205
|
+
if (!existsSync(path))
|
|
206
|
+
continue;
|
|
207
|
+
const raw = readFileSync(path, "utf-8");
|
|
208
|
+
const data = JSON.parse(raw);
|
|
209
|
+
const stanza = data.mcpServers?.[SERVER_NAME];
|
|
210
|
+
if (!stanza?.env)
|
|
211
|
+
continue;
|
|
212
|
+
stanza.env.OKTA_REFRESH_TOKEN = newToken;
|
|
213
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
214
|
+
try {
|
|
215
|
+
chmodSync(path, 0o600);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// ok on platforms where this is a no-op
|
|
219
|
+
}
|
|
175
220
|
}
|
|
176
|
-
catch {
|
|
177
|
-
//
|
|
221
|
+
catch (err) {
|
|
222
|
+
// Log to stderr (visible in Claude Desktop/Code logs) but don't
|
|
223
|
+
// fail the request.
|
|
224
|
+
console.error(`[MCP] Could not persist rotated refresh token to ${path}:`, err instanceof Error ? err.message : String(err));
|
|
178
225
|
}
|
|
179
226
|
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
// Log to stderr (visible in Claude Desktop logs) but don't fail the request.
|
|
182
|
-
console.error("[MCP] Could not persist rotated refresh token to ~/.mcp.json:", err instanceof Error ? err.message : String(err));
|
|
183
|
-
}
|
|
184
227
|
}
|
|
185
228
|
/**
|
|
186
229
|
* Mint a fresh access token via Okta's /token endpoint. Updates the
|
package/dist/tools/eng.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { trpcMutation, trpcQuery } from "../api-client.js";
|
|
2
|
+
import { trpcQuery } from "../api-client.js";
|
|
4
3
|
import { safeTool } from "./_helpers.js";
|
|
5
4
|
import { rangeSchema } from "./_schemas.js";
|
|
6
5
|
export function registerEngTools(server, allowed) {
|
|
@@ -11,25 +10,8 @@ export function registerEngTools(server, allowed) {
|
|
|
11
10
|
.describe("Time window. Short ranges (1h, 6h, 24h, 7d) are most useful. Default: 24h."),
|
|
12
11
|
}, safeTool(async ({ range }) => trpcQuery("mcp.systemDiagnostics", { range })));
|
|
13
12
|
}
|
|
14
|
-
if (allowed.has("revoke_api_key")) {
|
|
15
|
-
server.tool("revoke_api_key", "Immediately revoke every active MCP API key for a user. The user's loop_sk_* tokens in Claude Desktop stop working on the next tRPC call (401). Use for urgent access cutoff (suspected compromise, termination not yet processed in Okta). Routine offboarding should just deactivate the user in Okta — the 5-min reconciler picks that up.", {
|
|
16
|
-
email: z
|
|
17
|
-
.string()
|
|
18
|
-
.email()
|
|
19
|
-
.describe("Email of the Loop user to revoke."),
|
|
20
|
-
reason: z
|
|
21
|
-
.string()
|
|
22
|
-
.min(8)
|
|
23
|
-
.describe("Why you're revoking. Required — recorded in audit_log."),
|
|
24
|
-
}, safeTool(async ({ email, reason }) => trpcMutation("mcp.revokeApiKey", { email, reason })));
|
|
25
|
-
}
|
|
26
13
|
if (allowed.has("access_review")) {
|
|
27
|
-
server.tool("access_review", "Produce a user-access snapshot for SOC/security review. Lists every Loop user with role, status, SF linkage,
|
|
28
|
-
includeRevoked: z
|
|
29
|
-
.boolean()
|
|
30
|
-
.optional()
|
|
31
|
-
.describe("If true, also include users whose API keys have been revoked (for full historical picture). Default: false (active access only)."),
|
|
32
|
-
}, safeTool(async ({ includeRevoked }) => trpcQuery("mcp.accessReview", { includeRevoked })));
|
|
14
|
+
server.tool("access_review", "Produce a user-access snapshot for SOC/security review. Lists every active Loop user with role, status, SF linkage, and last-active date. The review itself is audited. Intended for quarterly access reviews. For token-level detail (issued/rotated/revoked), query the Okta system log directly.", {}, safeTool(async () => trpcQuery("mcp.accessReview")));
|
|
33
15
|
}
|
|
34
16
|
if (allowed.has("sync_local")) {
|
|
35
17
|
server.tool("sync_local", "Pull the latest changes from the remote repo into your local working tree. Runs `git pull --ff-only` in the repo at $LOOP_REPO_PATH. Use this after update_config / deploy_config writes commits directly to the remote branch so your local clone catches up.", {}, safeTool(async () => {
|