diff options
| -rw-r--r-- | mcp/README.org | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/mcp/README.org b/mcp/README.org new file mode 100644 index 0000000..c8cb49a --- /dev/null +++ b/mcp/README.org @@ -0,0 +1,81 @@ +#+TITLE: MCP Install Pipeline +#+AUTHOR: Craig Jennings + +Registers MCP servers at Claude Code's *user scope* (machine-wide, every project) from a single encrypted secrets bundle. One command — =make install-mcp= — decrypts the secrets, materializes the OAuth artifacts each server needs, expands placeholders in the server definitions, and registers anything not already present. Idempotent: re-running only adds what's missing. + +* Layout + +| File | Tracked? | Purpose | +|------+----------+---------| +| =install.py= | tracked | The installer. Decrypt → materialize OAuth artifacts → expand → register. | +| =servers.json= | tracked | Server definitions with =${VAR}= placeholders for secrets. Safe to commit (no secrets inline). | +| =secrets.env.gpg= | tracked | The secrets bundle, symmetrically encrypted (AES256). The only place secret values live. | +| =gcp-oauth.keys.json= | *gitignored* | Google OAuth client keys for =google-calendar-mcp=. Regenerated at install from a base64 var in the bundle; never committed. | + +The Google Docs token caches are written *outside the repo* at =~/.config/google-docs-mcp/<profile>/token.json= (mode 600), where =@a-bonus/google-docs-mcp= reads them at startup. + +* Secrets bundle shape + +=secrets.env.gpg= decrypts to a plain =KEY=value= file (one per line, =#= comments allowed). Two kinds of entry: + +1. *Plain secrets* referenced by =${VAR}= in =servers.json=: + - =GOOGLE_CLIENT_ID=, =GOOGLE_CLIENT_SECRET= — Google Docs OAuth client (both profiles share one client). + - =FIGMA_API_KEY=. + - =GOOGLE_KEEP_EMAIL=, =GOOGLE_KEEP_MASTER_TOKEN=. + +2. *Base64-bundled OAuth artifacts* the installer decodes and writes to disk (they are not referenced by =servers.json=; =install.py= consumes them directly): + - =GCP_OAUTH_KEYS_JSON_B64= → decoded to =mcp/gcp-oauth.keys.json= (mode 600). The installer then sets =GOOGLE_OAUTH_CREDENTIALS_PATH= to that path, which =servers.json= *does* reference for =google-calendar=. + - =GOOGLE_DOCS_PERSONAL_TOKEN_B64=, =GOOGLE_DOCS_WORK_TOKEN_B64= → decoded to =~/.config/google-docs-mcp/{personal,work}/token.json= (mode 600). These are the *refresh-token caches* — without them a fresh machine has no token and the stdio server falls back to interactive OAuth, which Claude Code's loader can't drive (it shows "Failed to connect"). + +** Re-encrypting the bundle + +Symmetric, AES256, passphrase-only (no key recipient): + +#+begin_src bash +gpg -c --cipher-algo AES256 secrets.env # produces secrets.env.gpg +rm secrets.env # never leave the plaintext around +#+end_src + +Decryption at install time goes through =gpg-agent= (pinentry prompts once, then caches per =gpg-agent.conf=). + +* Install flow + +=make install-mcp= runs =python3 mcp/install.py=, which: + +1. Checks =gpg= and =claude= are on =PATH=. +2. Decrypts =secrets.env.gpg= into an in-memory env dict. +3. Materializes OAuth artifacts: writes =gcp-oauth.keys.json= (mode 600) and the two Google Docs =token.json= caches (mode 600), popping those base64 vars from the env. +4. Reads =claude mcp list= to find already-registered servers. +5. For each server in =servers.json= not already registered: expands =${VAR}= placeholders against the secrets env (errors loudly if a placeholder is undefined), builds the right =claude mcp add --scope user= command for the transport, and runs it. +6. Prints an =Added / Skipped / Failed= tally; exits non-zero if any server failed. + +Idempotent — a server already in =claude mcp list= is skipped, so re-running after adding one new server only registers that one. + +* Transports + +=servers.json= keys each server by name; =type= selects the registration path: + +- *=http= / =sse=* (=linear=, =notion=, =slack-deepsat=) → =claude mcp add --scope user --transport <type> <name> <url>=. No local process; the URL is the endpoint. =slack-deepsat= is an =sse= server on =127.0.0.1= (a locally-running bridge). +- *=stdio=* (=google-calendar=, =drawio=, =google-docs-personal=, =google-docs-work=, =figma=, =google-keep=) → a local command (=npx= / =uvx=) launched per session. Env vars pass via repeated =-e KEY=value= flags placed *before* the =--= separator and the command, because commander.js parses =-e= as variadic and would otherwise eat the server name. + +* Token rotation (revoked Google refresh token) + +A Google refresh token dies when scopes are re-granted, the Connected App is removed, or the account password resets. Recovery is manual today: + +1. On one machine, run the server's =npx= command with the right env so it performs the interactive OAuth dance, follow the browser URL, then kill the process once =token.json= is written. +2. Base64-encode the new =token.json=. +3. Decrypt =secrets.env.gpg=, replace the matching =*_TOKEN_B64= var, re-encrypt (see above), recommit =secrets.env.gpg=. +4. =make install-mcp= on each machine re-materializes the cache from the bundle. + +(A =mcp/refresh-google-docs-token.sh <profile>= helper to chain this into one command is a tracked follow-up.) + +* Adding a new server + +1. Add the server block to =servers.json= with its =type= and any =${VAR}= placeholders. +2. Add the secret values for those vars to =secrets.env= and re-encrypt the bundle. +3. =make install-mcp= — it skips the already-registered servers and adds the new one. + +* HTTP vs stdio OAuth, and the gotchas + +- *HTTP/SSE servers* (=linear=, =notion=) carry their own hosted OAuth — the first tool call in a session triggers an in-client auth flow; nothing local to cache. If a connection fails, it is usually an expired in-client grant, re-authorized from the client, not from this pipeline. +- *stdio servers needing OAuth* (=google-docs-*=, =google-calendar=) cannot run an interactive browser flow under Claude Code's stdio loader. They depend on a *pre-seeded* token/keys file on disk — which is exactly why the installer materializes them from the bundle. A "Failed to connect" on one of these almost always means the on-disk token cache is missing or stale; rotate it (above) rather than re-running the server by hand. |
