summaryrefslogtreecommitdiff
path: root/link
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-09-18 13:03:16 +0100
committerIgor Soarez <igor@soarez.org>2026-09-18 13:03:16 +0100
commitfec40208281589b70228802914e98b3b4823924b (patch)
tree40d6c14580116b85e56f74eb99c92d99c7052260 /link
parent13cc065ec014b86369dfe4ad60d1a43fa06fd251 (diff)
link/claude: secrets in MCP specs, and a separate spec for Desktop
Two things a spec in ~/.config/claude/mcp-servers.d/ may now carry beyond what Claude Code takes, both resolved at link time: - "${szecret:NAME}" in any string value, replaced with `szecret get NAME` when the server is registered with Claude Code. For a remote server's Authorization header: Claude Code's own headersHelper is not run in a workspace without persisted trust, which includes $HOME, so a token fetched that way is simply missing. A secret that cannot be read skips that server, loudly. - "desktop", a whole spec given to Claude Desktop instead. Desktop only starts local stdio servers, so a layer that moves a server to HTTP for Claude Code can keep Desktop on a local copy.
Diffstat (limited to 'link')
-rwxr-xr-xlink/claude.sh43
1 files changed, 42 insertions, 1 deletions
diff --git a/link/claude.sh b/link/claude.sh
index b7b3f9e..56f3724 100755
--- a/link/claude.sh
+++ b/link/claude.sh
@@ -31,6 +31,21 @@ link "$config_dir/claude/CLAUDE.md" "$HOME/.claude/CLAUDE.md"
#
# Run by link.sh after other layers' link.sh, so their files are in place on a
# first link.
+#
+# Two things a spec may carry beyond what Claude Code takes, both resolved here:
+#
+# "${szecret:NAME}" anywhere in a string value, replaced with `szecret get
+# NAME` as the server is registered with Claude Code. For a
+# remote server's Authorization header: Claude Code's own
+# headersHelper is not run in a workspace without persisted
+# trust, which includes $HOME, so a token fetched there
+# would simply be missing. The secret ends up in
+# ~/.claude.json, which Claude Code keeps mode 600. A
+# secret that cannot be read skips that server, loudly.
+#
+# "desktop" a whole spec to give Claude Desktop instead of this one,
+# e.g. a local stdio server where Claude Code gets a remote
+# one. Claude Code never sees it.
mcp_dir="$xdg_config_dir/claude/mcp-servers.d"
mcp_manifest="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/claude-mcp-servers"
claude_desktop_dir="$HOME/Library/Application Support/Claude"
@@ -105,9 +120,34 @@ then
then
echo "Will register MCP server (user scope): $name"
else
+ if ! code_spec="$(python3 -c 'import json, re, subprocess, sys
+spec = json.loads(sys.argv[1])
+spec.pop("desktop", None)
+
+def secret(match):
+ name = match.group(1)
+ try:
+ return subprocess.run(["szecret", "get", name], check=True, capture_output=True, text=True).stdout.strip()
+ except (OSError, subprocess.CalledProcessError):
+ sys.exit("szecret get %s failed" % name)
+
+def expand(value):
+ if isinstance(value, str):
+ return re.sub(r"\$\{szecret:([A-Za-z0-9._-]+)\}", secret, value)
+ if isinstance(value, dict):
+ return {k: expand(v) for k, v in value.items()}
+ if isinstance(value, list):
+ return [expand(v) for v in value]
+ return value
+
+print(json.dumps(expand(spec)))' "$spec" 2>&1)"
+ then
+ red "MCP server (user scope) not registered: $name: $code_spec"
+ continue
+ fi
green "MCP server (user scope): $name"
claude mcp remove --scope user "$name" >/dev/null 2>&1 || true
- claude mcp add-json --scope user "$name" "$spec" >/dev/null
+ claude mcp add-json --scope user "$name" "$code_spec" >/dev/null
fi
done <<< "$mcp_wanted"
fi
@@ -149,6 +189,7 @@ for line in os.environ["MCP_WANTED"].splitlines():
continue
name, spec = line.split("\t", 1)
spec = json.loads(spec)
+ spec = spec.get("desktop", spec)
command = spec.get("command")
if command:
if not os.path.isabs(command):