@loopops/mcp-server 2.0.1 → 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.
@@ -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;
@@ -152,11 +152,37 @@ function isRetryable(err) {
152
152
  return true;
153
153
  return false;
154
154
  }
155
+ /**
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
+ }
155
180
  /**
156
181
  * Persist a rotated refresh token across every config location we know
157
182
  * users store MCP settings in:
158
- * - ~/.mcp.json (Claude Desktop)
183
+ * - ~/.mcp.json (CLI canonical, also legacy Claude Desktop path)
159
184
  * - ~/.claude/settings.json (Claude Code)
185
+ * - OS-specific Claude Desktop config (macOS/Windows/Linux)
160
186
  *
161
187
  * We update whichever files have the loop-operations stanza. Missing
162
188
  * files are skipped silently. This mirrors @loopops/mcp-cli's
@@ -171,6 +197,9 @@ function persistRotatedRefreshToken(newToken) {
171
197
  join(homedir(), ".mcp.json"),
172
198
  join(homedir(), ".claude", "settings.json"),
173
199
  ];
200
+ const desktop = claudeDesktopConfigPath();
201
+ if (desktop)
202
+ paths.push(desktop);
174
203
  for (const path of paths) {
175
204
  try {
176
205
  if (!existsSync(path))
package/dist/tools/eng.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
- import { z } from "zod";
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, active/revoked key counts, and last-active date. The review itself is audited auditors can see who produced it and when. Intended for quarterly access reviews.", {
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 () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopops/mcp-server",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Loop Operations MCP Server — AI skills for RevOps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",