Skip to content

Commit d5de413

Browse files
Merge pull request #4 from Spacecraft-Software/add-fill-keys-script
Add bin/fill-keys.nu — fill API-key placeholders into a gitignored dist/
2 parents 81aa723 + 485a1bb commit d5de413

6 files changed

Lines changed: 527 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Key-filled configs produced by bin/fill-keys.nu — never commit real secrets.
2+
/dist/

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ Every host template declares all nine. Two groups:
7373
- Antigravity's file uses **2-space** indentation; VS Code's uses **tabs**. Preserve each file's existing style. Host directory names are **case-sensitive and canonical** (`Antigravity/`, `VSCode/`) — do not reintroduce lowercase `antigravity/` or `.vscode/` variants (a past PR did; they were consolidated).
7474
- Never commit a real secret — templates carry placeholders (`YOUR_CONTEXT7_API_KEY`, `YOUR_GITHUB_PAT`, `YOUR_BRAVE_API_KEY`) and the `/path/to/your/workspace` path. Servers needing them stay inert until filled in locally.
7575
- Templates are the **canonical superset**; the maintainer's live machine runs the three real servers only. When changing a server, update **every** host template in its dialect (and the live config too, for the three real servers).
76+
77+
## Tooling
78+
79+
`bin/fill-keys.{nu,sh,ion}` substitute the four placeholder tokens (`YOUR_CONTEXT7_API_KEY`, `YOUR_BRAVE_API_KEY`, `YOUR_GITHUB_PAT`, `/path/to/your/workspace`) with values from env vars (prompting for any unset ones when interactive) and write filled copies into a **gitignored `dist/` mirror** — they never edit the tracked templates, so the no-secrets rule holds. There are three parallel ports (Nushell, POSIX/Bash/Brush, Ion) with identical behavior — **change all three together**, plus their shared host-file list (the docs and VS Code's `${input:}`/`${workspaceFolder}` fields are deliberately left out). The `.sh`/`.ion` ports shell out to `sd` (literal `-s` mode); the `.nu` port uses native string ops. These are the only executables (`755`); everything else is `644` data. Shell-specific gotchas worth knowing if you edit them: Ion's `test -t` is unreliable (use `tty -s`), Ion eats `-h`/`--help`, and Ion's `test` needs the POSIX `x`-prefix guard for `--`-leading operands.

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,43 @@ need a token or path filled in before they work (see Notes).
4646
- Schemas differ per host (e.g. Qwen uses `httpUrl`, Codex uses `http_headers`, Copilot
4747
CLI omits the `mcpServers` wrapper). See `CLAUDE.md` for the full per-host schema table.
4848

49+
## Filling in your keys
50+
51+
`bin/fill-keys.*` substitutes the placeholders with your real values and writes
52+
ready-to-use copies into a gitignored `dist/` mirror — the tracked templates are never
53+
modified, so no secret is ever committed. The same tool is provided for three shells
54+
(pick whichever you run); all behave identically:
55+
56+
| Script | Shell | Notes |
57+
|--------|-------|-------|
58+
| `bin/fill-keys.nu` | Nushell | no external deps (native string ops) |
59+
| `bin/fill-keys.sh` | POSIX sh / Bash / Brush | needs [`sd`](https://github.com/chmln/sd) |
60+
| `bin/fill-keys.ion` | Ion | needs `sd`; Ion eats `-h`/`--help` itself, so see the file header for usage |
61+
62+
| Env var | Fills | Server |
63+
|---------|-------|--------|
64+
| `CONTEXT7_API_KEY` | `YOUR_CONTEXT7_API_KEY` | context7 |
65+
| `BRAVE_API_KEY` | `YOUR_BRAVE_API_KEY` | brave-search |
66+
| `GITHUB_PAT` | `YOUR_GITHUB_PAT` | github |
67+
| `WORKSPACE_PATH` | `/path/to/your/workspace` | filesystem |
68+
69+
```sh
70+
# Provide values via env vars (any you omit are prompted for, or left as placeholders
71+
# when run non-interactively). Use whichever script matches your shell:
72+
CONTEXT7_API_KEY=ctx7sk-... BRAVE_API_KEY=... GITHUB_PAT=ghp_... \
73+
WORKSPACE_PATH=/home/you/work \
74+
nu bin/fill-keys.nu # or: sh bin/fill-keys.sh / ion bin/fill-keys.ion
75+
76+
nu bin/fill-keys.nu --help # options (Nushell/POSIX; Ion: see header)
77+
nu bin/fill-keys.nu --out /tmp/mcp # write somewhere else
78+
```
79+
80+
The script prints each filled file's intended install destination. Copy the relevant
81+
`dist/<Host>/…` file to that path (whole-file for most hosts; for hosts whose live
82+
config holds other settings — Codex, Claude Code, Grok, Kimi, OpenClaude — merge the
83+
MCP block in). Omitted keys keep their placeholder, so that server simply stays inert.
84+
VS Code is copied unchanged: it prompts for the Context7/Brave keys itself.
85+
4986
## Project Posture
5087

5188
`mcp-servers` is a **personal hobby project** under the

bin/fill-keys.ion

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env ion
2+
# SPDX-FileCopyrightText: 2026 Mohamed Hammad <Mohamed.Hammad@SpacecraftSoftware.org>
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
#
5+
# fill-keys.ion — Ion (Redox) port of fill-keys.nu.
6+
#
7+
# Fills the MCP config templates' placeholders with real values and writes ready-to-use
8+
# copies into a gitignored output dir (default: dist/). Values come from env vars; any
9+
# unset ones are prompted for (hidden input) when interactive, or left as placeholders
10+
# when not (CI/agent). The tracked templates are never modified.
11+
#
12+
# Env var Fills placeholder Server
13+
# CONTEXT7_API_KEY YOUR_CONTEXT7_API_KEY context7
14+
# BRAVE_API_KEY YOUR_BRAVE_API_KEY brave-search
15+
# GITHUB_PAT YOUR_GITHUB_PAT github
16+
# WORKSPACE_PATH /path/to/your/workspace filesystem
17+
#
18+
# Requires: sd (cargo install sd / nix run nixpkgs#sd).
19+
#
20+
# Note: `test "x$v" = "x..."` uses the POSIX x-prefix guard — Ion's `test` treats a bare
21+
# leading `--word` operand as an option flag, so comparands must not start with `-`.
22+
23+
fn usage
24+
echo "Usage: fill-keys.ion [--out DIR]"
25+
echo ""
26+
echo " --out DIR output directory (gitignored), relative to the repo root [default: dist]"
27+
echo " -h, --help show this help"
28+
echo ""
29+
echo "Env vars: CONTEXT7_API_KEY, BRAVE_API_KEY, GITHUB_PAT, WORKSPACE_PATH"
30+
end
31+
32+
# Hidden/plain prompt on the controlling terminal; echoes the typed value to stdout.
33+
fn promptval label:str secret:str
34+
if test "x$secret" = "x1"
35+
let old = $(stty -g < /dev/tty)
36+
stty -echo < /dev/tty
37+
printf '%s' $label > /dev/tty
38+
let reply = $(head -n 1 < /dev/tty)
39+
stty $old < /dev/tty
40+
printf '\n' > /dev/tty
41+
echo $reply
42+
else
43+
printf '%s' $label > /dev/tty
44+
let reply = $(head -n 1 < /dev/tty)
45+
echo $reply
46+
end
47+
end
48+
49+
# --- argument parsing ---
50+
# @args[0] is the script path; Ion only lets us read the args array whole (@args),
51+
# so capture the first element in the loop. (Ion eats -h/--help as its own flags, so
52+
# the --help branch below is best-effort; see the header comment for usage.)
53+
let out = "dist"
54+
let want_out = 0
55+
let idx = 0
56+
let script = ""
57+
for a in @args
58+
if test "x$idx" = "x0"
59+
let script = $a
60+
let idx = 1
61+
else if test "x$want_out" = "x1"
62+
let out = $a
63+
let want_out = 0
64+
else if test "x$a" = "x--out"
65+
let want_out = 1
66+
else if test "x$a" = "x--help"
67+
usage
68+
exit 0
69+
else if test "x$a" = "x-h"
70+
usage
71+
exit 0
72+
end
73+
end
74+
75+
# --- sd preflight ---
76+
let have_sd = 0
77+
if exists -b sd
78+
let have_sd = 1
79+
end
80+
if test "x$have_sd" = "x0"
81+
printf 'error: sd not found. Install it: cargo install sd (or: nix run nixpkgs#sd)\n' > /dev/stderr
82+
exit 1
83+
end
84+
85+
# --- paths ---
86+
let sdir = $(dirname $script)
87+
let repo = $(cd $sdir/.. && pwd)
88+
let first = $(printf '%s' $out | head -c 1)
89+
let out_root = "$repo/$out"
90+
if test "x$first" = "x/"
91+
let out_root = $out
92+
end
93+
94+
# --- interactive detection (tty -s is reliable in Ion; test -t is not) ---
95+
let interactive = 0
96+
if tty -s
97+
let interactive = 1
98+
end
99+
if exists -s CI
100+
let interactive = 0
101+
end
102+
if exists -s CLAUDECODE
103+
let interactive = 0
104+
end
105+
106+
# --- resolve each token: env var, else prompt, else empty ---
107+
let v_ctx = ""
108+
if exists -s CONTEXT7_API_KEY
109+
let v_ctx = $CONTEXT7_API_KEY
110+
end
111+
if test -z "$v_ctx" && test "x$interactive" = "x1"
112+
let v_ctx = $(promptval "CONTEXT7_API_KEY [blank to skip]: " "1")
113+
end
114+
115+
let v_brave = ""
116+
if exists -s BRAVE_API_KEY
117+
let v_brave = $BRAVE_API_KEY
118+
end
119+
if test -z "$v_brave" && test "x$interactive" = "x1"
120+
let v_brave = $(promptval "BRAVE_API_KEY [blank to skip]: " "1")
121+
end
122+
123+
let v_pat = ""
124+
if exists -s GITHUB_PAT
125+
let v_pat = $GITHUB_PAT
126+
end
127+
if test -z "$v_pat" && test "x$interactive" = "x1"
128+
let v_pat = $(promptval "GITHUB_PAT [blank to skip]: " "1")
129+
end
130+
131+
let v_ws = ""
132+
if exists -s WORKSPACE_PATH
133+
let v_ws = $WORKSPACE_PATH
134+
end
135+
if test -z "$v_ws" && test "x$interactive" = "x1"
136+
let v_ws = $(promptval "WORKSPACE_PATH [blank to skip]: " "0")
137+
end
138+
139+
let filled = 0
140+
let filled_names = ""
141+
let skipped_names = ""
142+
if test -n "$v_ctx"
143+
let filled += 1
144+
let filled_names ++= " CONTEXT7_API_KEY"
145+
else
146+
let skipped_names ++= " CONTEXT7_API_KEY"
147+
end
148+
if test -n "$v_brave"
149+
let filled += 1
150+
let filled_names ++= " BRAVE_API_KEY"
151+
else
152+
let skipped_names ++= " BRAVE_API_KEY"
153+
end
154+
if test -n "$v_pat"
155+
let filled += 1
156+
let filled_names ++= " GITHUB_PAT"
157+
else
158+
let skipped_names ++= " GITHUB_PAT"
159+
end
160+
if test -n "$v_ws"
161+
let filled += 1
162+
let filled_names ++= " WORKSPACE_PATH"
163+
else
164+
let skipped_names ++= " WORKSPACE_PATH"
165+
end
166+
167+
# --- write filled copies ---
168+
let written = 0
169+
for src in [ "Antigravity/mcp_config.json" "VSCode/mcp.json" "GitHubCopilotCLI/mcp-config.json" "ClaudeCode/.mcp.json" "OpenClaude/.mcp.json" "Codex/config.toml" "Grok/config.toml" "Kimi/config.toml" "Gemini/settings.json" "Gemini/mcp-server-enablement.json" "Qwen/settings.json" "OpenCode/opencode.jsonc" "Goose/config.yaml" ]
170+
let from = "$repo/$src"
171+
let have = 0
172+
if exists -f $from
173+
let have = 1
174+
end
175+
if test "x$have" = "x0"
176+
printf 'error: missing template %s\n' $src > /dev/stderr
177+
exit 1
178+
end
179+
let dst = "$out_root/$src"
180+
mkdir -p $(dirname $dst)
181+
cp $from $dst
182+
if test -n "$v_ctx"
183+
sd -s "YOUR_CONTEXT7_API_KEY" $v_ctx $dst
184+
end
185+
if test -n "$v_brave"
186+
sd -s "YOUR_BRAVE_API_KEY" $v_brave $dst
187+
end
188+
if test -n "$v_pat"
189+
sd -s "YOUR_GITHUB_PAT" $v_pat $dst
190+
end
191+
if test -n "$v_ws"
192+
sd -s "/path/to/your/workspace" $v_ws $dst
193+
end
194+
let written += 1
195+
end
196+
197+
# --- summary ---
198+
printf 'Filled %s of 4 placeholders into %s/ (%s files)\n' $filled $out_root $written
199+
if test -n "$filled_names"
200+
printf 'Filled:%s\n' $filled_names
201+
end
202+
if test -n "$skipped_names"
203+
printf 'Left as placeholders, those servers stay inert:%s\n' $skipped_names
204+
end
205+
echo ""
206+
echo "Install destinations:"
207+
for line in [ "Antigravity/mcp_config.json -> Antigravity, editor-managed" "VSCode/mcp.json -> <workspace>/.vscode/mcp.json" "GitHubCopilotCLI/mcp-config.json -> ~/.copilot/mcp-config.json" "ClaudeCode/.mcp.json -> ~/.claude.json mcpServers" "OpenClaude/.mcp.json -> ~/.openclaude.json mcpServers" "Codex/config.toml -> ~/.codex/config.toml" "Grok/config.toml -> ~/.grok/config.toml" "Kimi/config.toml -> ~/.kimi-code/config.toml" "Gemini/settings.json -> ~/.gemini/settings.json" "Gemini/mcp-server-enablement.json -> ~/.gemini/mcp-server-enablement.json" "Qwen/settings.json -> ~/.qwen/settings.json" "OpenCode/opencode.jsonc -> ~/.config/opencode/opencode.jsonc" "Goose/config.yaml -> ~/.config/goose/config.yaml" ]
208+
echo " $line"
209+
end

bin/fill-keys.nu

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env nu
2+
# SPDX-FileCopyrightText: 2026 Mohamed Hammad <Mohamed.Hammad@SpacecraftSoftware.org>
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
#
5+
# fill-keys.nu — fill the MCP config templates' placeholders with real values and
6+
# write ready-to-use copies into a gitignored output dir (default: dist/).
7+
#
8+
# Values are read from environment variables; any that are unset are prompted for
9+
# (hidden input) when run interactively, or left as placeholders when not (CI/agent).
10+
# The tracked templates are never modified — secrets only ever land in the output dir.
11+
#
12+
# Env var Fills placeholder Server
13+
# CONTEXT7_API_KEY YOUR_CONTEXT7_API_KEY context7
14+
# BRAVE_API_KEY YOUR_BRAVE_API_KEY brave-search
15+
# GITHUB_PAT YOUR_GITHUB_PAT github
16+
# WORKSPACE_PATH /path/to/your/workspace filesystem
17+
#
18+
# Usage:
19+
# CONTEXT7_API_KEY=ctx7sk-... BRAVE_API_KEY=... nu bin/fill-keys.nu
20+
# nu bin/fill-keys.nu --out /tmp/mcp # custom output dir
21+
# nu bin/fill-keys.nu --help
22+
23+
# Each placeholder, the env var that fills it, and whether it is a secret (hidden prompt).
24+
const TOKENS = [
25+
[placeholder env_var secret];
26+
["YOUR_CONTEXT7_API_KEY" "CONTEXT7_API_KEY" true ]
27+
["YOUR_BRAVE_API_KEY" "BRAVE_API_KEY" true ]
28+
["YOUR_GITHUB_PAT" "GITHUB_PAT" true ]
29+
["/path/to/your/workspace" "WORKSPACE_PATH" false]
30+
]
31+
32+
# Host config templates and where each filled copy is meant to be installed.
33+
# VS Code and the Gemini enablement file carry no placeholders — they are copied verbatim.
34+
const HOSTS = [
35+
[src live];
36+
["Antigravity/mcp_config.json" "(Antigravity — editor-managed)" ]
37+
["VSCode/mcp.json" "<workspace>/.vscode/mcp.json" ]
38+
["GitHubCopilotCLI/mcp-config.json" "~/.copilot/mcp-config.json" ]
39+
["ClaudeCode/.mcp.json" "~/.claude.json (mcpServers)" ]
40+
["OpenClaude/.mcp.json" "~/.openclaude.json (mcpServers)" ]
41+
["Codex/config.toml" "~/.codex/config.toml" ]
42+
["Grok/config.toml" "~/.grok/config.toml" ]
43+
["Kimi/config.toml" "~/.kimi-code/config.toml" ]
44+
["Gemini/settings.json" "~/.gemini/settings.json" ]
45+
["Gemini/mcp-server-enablement.json" "~/.gemini/mcp-server-enablement.json" ]
46+
["Qwen/settings.json" "~/.qwen/settings.json" ]
47+
["OpenCode/opencode.jsonc" "~/.config/opencode/opencode.jsonc" ]
48+
["Goose/config.yaml" "~/.config/goose/config.yaml" ]
49+
]
50+
51+
# Read an env var, returning null when unset or empty.
52+
def env-value [name: string]: nothing -> any {
53+
let v = (try { $env | get $name } catch { null })
54+
if ($v | is-empty) { null } else { $v }
55+
}
56+
57+
# Resolve every token to a value: env var first, then an interactive prompt, else null.
58+
def resolve-tokens [interactive: bool]: nothing -> table {
59+
$TOKENS | each {|t|
60+
mut value = (env-value $t.env_var)
61+
if ($value == null) and $interactive {
62+
let entered = (
63+
if $t.secret {
64+
input --suppress-output $"($t.env_var) [hidden, blank to skip]: "
65+
} else {
66+
input $"($t.env_var) [blank to skip]: "
67+
}
68+
)
69+
# `input --suppress-output` leaves the cursor on the prompt line.
70+
if $t.secret { print "" }
71+
$value = (if ($entered | is-empty) { null } else { $entered })
72+
}
73+
{placeholder: $t.placeholder, value: $value, filled: ($value != null)}
74+
}
75+
}
76+
77+
# Fill placeholders in the templates and write copies under `out`.
78+
def main [
79+
--out: string = "dist" # output directory (gitignored), relative to the repo root
80+
] {
81+
let repo = ($env.FILE_PWD | path dirname)
82+
let interactive = (
83+
(is-terminal --stdin)
84+
and ((env-value "CI") == null)
85+
and ((env-value "CLAUDECODE") == null)
86+
)
87+
88+
let tokens = (resolve-tokens $interactive)
89+
let active = ($tokens | where filled)
90+
91+
let out_root = if ($out | path type) == "dir" or ($out | str starts-with "/") {
92+
$out
93+
} else {
94+
$repo | path join $out
95+
}
96+
97+
for h in $HOSTS {
98+
let src = ($repo | path join $h.src)
99+
if not ($src | path exists) {
100+
print --stderr $"error: missing template ($h.src)"
101+
exit 1
102+
}
103+
let dst = ($out_root | path join $h.src)
104+
mkdir ($dst | path dirname)
105+
mut content = (open --raw $src)
106+
for t in $active {
107+
$content = ($content | str replace --all $t.placeholder $t.value)
108+
}
109+
$content | save --force $dst
110+
}
111+
112+
# Summary to stdout.
113+
print $"Filled ($active | length) of ($tokens | length) placeholders → ($out_root)/"
114+
print ($tokens | select placeholder filled)
115+
let skipped = ($tokens | where not filled | get placeholder)
116+
if ($skipped | is-not-empty) {
117+
print $"Left as placeholders — those servers stay inert: ($skipped | str join ', ')"
118+
}
119+
print ""
120+
print "Install destinations:"
121+
print ($HOSTS | select src live)
122+
}

0 commit comments

Comments
 (0)