@forhuman/flowmcp 0.1.0

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/lib/clients.sh ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ # Resolves config file paths for supported MCP clients and merges the
3
+ # mcpServers block into them without clobbering unrelated entries.
4
+
5
+ wfw_os() {
6
+ case "$(uname -s)" in
7
+ Darwin) echo "macos" ;;
8
+ Linux) echo "linux" ;;
9
+ MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
10
+ *) echo "unknown" ;;
11
+ esac
12
+ }
13
+
14
+ # wfw_client_config_path <client> <scope>
15
+ # client: claude-code | claude-desktop | cursor
16
+ # scope: user | project (project scope resolves relative to $PWD)
17
+ wfw_client_config_path() {
18
+ local client="$1" scope="$2" os
19
+ os="$(wfw_os)"
20
+ case "$client" in
21
+ claude-code)
22
+ if [[ "$scope" == "project" ]]; then
23
+ echo "$PWD/.mcp.json"
24
+ else
25
+ echo "$HOME/.claude.json"
26
+ fi
27
+ ;;
28
+ claude-desktop)
29
+ if [[ "$scope" == "project" ]]; then
30
+ echo "error: claude-desktop has no project scope" >&2
31
+ return 1
32
+ fi
33
+ case "$os" in
34
+ macos) echo "$HOME/Library/Application Support/Claude/claude_desktop_config.json" ;;
35
+ linux) echo "$HOME/.config/Claude/claude_desktop_config.json" ;;
36
+ windows) echo "$APPDATA/Claude/claude_desktop_config.json" ;;
37
+ *) echo "error: unsupported OS for claude-desktop" >&2; return 1 ;;
38
+ esac
39
+ ;;
40
+ cursor)
41
+ if [[ "$scope" == "project" ]]; then
42
+ echo "$PWD/.cursor/mcp.json"
43
+ else
44
+ echo "$HOME/.cursor/mcp.json"
45
+ fi
46
+ ;;
47
+ *)
48
+ echo "error: unknown client '$client' (expected claude-code|claude-desktop|cursor)" >&2
49
+ return 1
50
+ ;;
51
+ esac
52
+ }
53
+
54
+ # wfw_client_merge_server <config_path> <server_name> <server_json> [--force]
55
+ # Merges .mcpServers[server_name] = server_json into config_path, preserving
56
+ # every other key in the file. Refuses to overwrite an existing entry unless
57
+ # --force is passed. Creates parent dirs and a .bak backup of any existing file.
58
+ wfw_client_merge_server() {
59
+ local config_path="$1" server_name="$2" server_json="$3" force="${4:-}"
60
+ local dir tmp existing
61
+ dir="$(dirname "$config_path")"
62
+ mkdir -p "$dir"
63
+
64
+ if [[ -f "$config_path" ]]; then
65
+ if ! jq empty "$config_path" >/dev/null 2>&1; then
66
+ echo "error: $config_path is not valid JSON — refusing to touch it" >&2
67
+ return 1
68
+ fi
69
+ existing="$(jq -r --arg n "$server_name" '.mcpServers[$n] // empty' "$config_path")"
70
+ if [[ -n "$existing" && "$force" != "--force" ]]; then
71
+ echo "error: '$server_name' already exists in $config_path (use --force to overwrite)" >&2
72
+ return 1
73
+ fi
74
+ cp "$config_path" "$config_path.bak"
75
+ else
76
+ echo '{}' > "$config_path"
77
+ fi
78
+
79
+ tmp="$(mktemp)"
80
+ jq --arg n "$server_name" --argjson s "$server_json" \
81
+ '.mcpServers = ((.mcpServers // {}) + {($n): $s})' \
82
+ "$config_path" > "$tmp" && mv "$tmp" "$config_path"
83
+ }
84
+
85
+ # wfw_build_server_json <org> <auth_method> — the mcpServers entry for an
86
+ # org, shared by install.sh and rename.sh so both stay in sync.
87
+ wfw_build_server_json() {
88
+ local org="$1" auth_method="$2"
89
+ if [[ "$auth_method" == "mcp-remote" ]]; then
90
+ local remote_dir
91
+ remote_dir="$(wfw_mcp_remote_dir "$org")"
92
+ jq -n --arg url "$WFW_MCP_URL" --arg dir "$remote_dir" \
93
+ '{command: "npx", args: ["-y", "mcp-remote", $url, "--resource", $url], env: {MCP_REMOTE_CONFIG_DIR: $dir}}'
94
+ else
95
+ local run_mcp_path="$WFW_COMMANDS_DIR/run-mcp.sh"
96
+ chmod +x "$run_mcp_path" 2>/dev/null || true
97
+ jq -n --arg cmd "$run_mcp_path" --arg org "$org" '{command: $cmd, args: [$org]}'
98
+ fi
99
+ }
100
+
101
+ wfw_client_remove_server() {
102
+ local config_path="$1" server_name="$2" tmp
103
+ [[ -f "$config_path" ]] || return 0
104
+ tmp="$(mktemp)"
105
+ jq --arg n "$server_name" 'if .mcpServers then .mcpServers |= del(.[$n]) else . end' \
106
+ "$config_path" > "$tmp" && mv "$tmp" "$config_path"
107
+ }
package/lib/common.sh ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # Shared paths, env, and sanity checks for flowmcp.
3
+ # Sourced by every command script. Never echo secrets from anything in lib/.
4
+
5
+ set -euo pipefail
6
+
7
+ WFW_HOME="${WFW_HOME:-$HOME/.flowmcp}"
8
+ WFW_PROFILES_DIR="$WFW_HOME/profiles"
9
+ WFW_SECRETS_DIR="$WFW_HOME/secrets" # only used by the file-fallback backend
10
+ WFW_AUDIT_DIR="$WFW_HOME/audit"
11
+ WFW_MCP_REMOTE_BASE_DIR="$WFW_HOME/mcp-remote" # per-org isolated mcp-remote token storage
12
+ WFW_KEYCHAIN_SERVICE="flowmcp"
13
+ WFW_MCP_URL="https://mcp.webflow.com/mcp" # Webflow's official hosted MCP server
14
+
15
+ wfw_ensure_dirs() {
16
+ umask 077
17
+ mkdir -p "$WFW_PROFILES_DIR" "$WFW_SECRETS_DIR" "$WFW_AUDIT_DIR" "$WFW_MCP_REMOTE_BASE_DIR"
18
+ }
19
+
20
+ # wfw_mcp_remote_dir <org> — isolated MCP_REMOTE_CONFIG_DIR so each org's
21
+ # OAuth session against the same mcp.webflow.com URL never collides with
22
+ # another org's.
23
+ wfw_mcp_remote_dir() { echo "$WFW_MCP_REMOTE_BASE_DIR/$1"; }
24
+
25
+ wfw_require_jq() {
26
+ command -v jq >/dev/null 2>&1 || {
27
+ echo "error: jq is required but not installed (https://jqlang.org)" >&2
28
+ exit 1
29
+ }
30
+ }
31
+
32
+ wfw_require_curl() {
33
+ command -v curl >/dev/null 2>&1 || {
34
+ echo "error: curl is required but not installed" >&2
35
+ exit 1
36
+ }
37
+ }
38
+
39
+ wfw_require_npx() {
40
+ command -v npx >/dev/null 2>&1 || {
41
+ echo "error: npx (Node.js) is required for the OAuth flow (connect) but not installed" >&2
42
+ exit 1
43
+ }
44
+ }
45
+
46
+ # wfw_json_mode <explicit-flag> — true if --json was passed OR stdout isn't
47
+ # a real TTY (piped, captured by an agent's tool call, redirected to a file).
48
+ # Lets every read command default to machine-readable output the moment a
49
+ # human isn't directly watching the terminal, without requiring the caller
50
+ # to know to pass --json.
51
+ wfw_json_mode() {
52
+ [[ "$1" == "1" ]] && return 0
53
+ [[ ! -t 1 ]] && return 0
54
+ return 1
55
+ }
56
+
57
+ wfw_valid_org() {
58
+ [[ "$1" =~ ^[a-z0-9][a-z0-9_-]*$ ]]
59
+ }
60
+
61
+ wfw_profile_path() { echo "$WFW_PROFILES_DIR/$1.json"; }
62
+
63
+ wfw_now() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
64
+
65
+ wfw_ensure_dirs
66
+ wfw_require_jq
package/lib/i18n.sh ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env bash
2
+ # Minimal i18n: detects/stores a human's language preference once, on their
3
+ # first interactive run. Never touches JSON output — agents always get
4
+ # English with fixed keys (see schema.sh). This only affects the
5
+ # human-readable text a person actually reads (banner, --help, and the
6
+ # plain-language guidance printed by add/connect/secret-set/rotate/list).
7
+
8
+ WFW_LANG_FILE="$WFW_HOME/lang"
9
+
10
+ # wfw_lang — resolves the active language: WFW_LANG env override, then the
11
+ # saved preference file, then "en". Never prompts — safe to call anywhere,
12
+ # including from a non-interactive/agent context.
13
+ wfw_lang() {
14
+ if [[ -n "${WFW_LANG:-}" ]]; then
15
+ echo "$WFW_LANG"
16
+ return
17
+ fi
18
+ if [[ -f "$WFW_LANG_FILE" ]]; then
19
+ cat "$WFW_LANG_FILE"
20
+ return
21
+ fi
22
+ echo "en"
23
+ }
24
+
25
+ # wfw_prompt_lang_if_needed — asks once, only for a real interactive human
26
+ # (both stdin and stdout are TTYs), with no saved preference and no WFW_LANG
27
+ # override already set. Never runs for an agent's non-interactive Bash call
28
+ # — same rule as secret-set/rotate: no command may block a non-interactive
29
+ # run.
30
+ wfw_prompt_lang_if_needed() {
31
+ [[ -n "${WFW_LANG:-}" ]] && return 0
32
+ [[ -f "$WFW_LANG_FILE" ]] && return 0
33
+ [[ -t 0 && -t 1 ]] || return 0
34
+
35
+ echo "Choose a language / Elige un idioma:"
36
+ echo " [1] English"
37
+ echo " [2] Español"
38
+ local choice
39
+ read -r -p "> " choice
40
+ case "$choice" in
41
+ 2|es|Es|ES) echo "es" > "$WFW_LANG_FILE" ;;
42
+ *) echo "en" > "$WFW_LANG_FILE" ;;
43
+ esac
44
+ }
45
+
46
+ # wfw_t <key> [args...] — looks up a human-facing message in the active
47
+ # language and printf-substitutes any extra args into it (%s placeholders).
48
+ # English is the fallback for any key not yet translated in Spanish, and for
49
+ # any unknown key entirely (returns the key itself, so a typo is visible,
50
+ # not silent).
51
+ wfw_t() {
52
+ local key="$1"; shift || true
53
+ local msg
54
+ if [[ "$(wfw_lang)" == "es" ]]; then
55
+ msg="$(wfw_t_es "$key")"
56
+ else
57
+ msg="$(wfw_t_en "$key")"
58
+ fi
59
+ if [[ $# -gt 0 ]]; then
60
+ printf -- "$msg\n" "$@"
61
+ else
62
+ printf '%s\n' "$msg"
63
+ fi
64
+ }
65
+
66
+ wfw_t_es() {
67
+ local key="$1"
68
+ case "$key" in
69
+ tagline) echo "conecta Webflow a tu agente de IA, sin exponer tus tokens" ;;
70
+ usage) echo "USO" ;;
71
+ onboarding) echo "PRIMEROS PASOS" ;;
72
+ daily_use) echo "USO DIARIO" ;;
73
+ security_1) echo "seguridad · esta herramienta nunca acepta un token como argumento y" ;;
74
+ security_2) echo "nunca lo imprime. secret-set/rotate requieren una terminal interactiva real;" ;;
75
+ security_3) echo "connect abre un navegador real (via mcp-remote, OAuth propio de Webflow — sin apps que configurar)." ;;
76
+ cmd_connect) echo "<org> [--label NOMBRE] — agrega un cliente por navegador, sin configuración (recomendado)" ;;
77
+ cmd_add) echo "<org> [--label NOMBRE] — registra solo los metadatos del org" ;;
78
+ cmd_secret_set) echo "<org> — pega un token manualmente (alternativa sin navegador)" ;;
79
+ cmd_rotate) echo "<org> — reemplaza un token guardado" ;;
80
+ cmd_list) echo "lista los orgs registrados + último estado de test" ;;
81
+ cmd_inspect) echo "<org> [--live] — muestra el detalle del perfil" ;;
82
+ cmd_test) echo "<org> — valida las credenciales guardadas" ;;
83
+ cmd_install) echo "<org> <client> [--scope user|project] [--force]" ;;
84
+ cmd_install_clients) echo "client: claude-code | claude-desktop | cursor" ;;
85
+ cmd_remove) echo "<org> --yes [--from client:scope]... — elimina perfil + credenciales" ;;
86
+ cmd_debug) echo "<org> — diagnostica una conexión rota" ;;
87
+ cmd_rename) echo "<old-org> <new-org> — sin necesidad de volver a autenticar" ;;
88
+ cmd_schema) echo "referencia de comandos/JSON legible por máquina, para agentes" ;;
89
+ cmd_lang) echo "[en|es] — ver o cambiar el idioma de la salida para humanos" ;;
90
+ msg_no_orgs) echo "Todavía no hay orgs registrados. Corre 'flowmcp add <org>' para empezar." ;;
91
+ msg_reconnecting) echo "reconectando org existente '%s'..." ;;
92
+ msg_connect_opening) echo "Abriendo tu navegador para que '%s' apruebe el acceso a Webflow..." ;;
93
+ msg_connect_ctrlc1) echo "En cuanto lo veas conectado abajo, presiona Ctrl+C para volver aquí — tu" ;;
94
+ msg_connect_ctrlc2) echo "sesión ya queda guardada en disco en ese momento." ;;
95
+ msg_add_recommend) echo "primero intenta 'flowmcp connect %s' — inicia sesión por navegador, sin copiar/pegar ningún token" ;;
96
+ msg_add_no_browser) echo "¿Sin navegador disponible? Corre esto tú mismo, en tu propia terminal (no a través de un agente):" ;;
97
+ msg_add_run_yourself) echo "corre esto tú mismo, en tu propia terminal (no a través de un agente):" ;;
98
+ msg_add_explain1) echo "Te va a pedir el token de la API de Webflow con entrada oculta y lo guardará" ;;
99
+ msg_add_explain2) echo "directamente en %s. El token nunca se pasa como argumento de comando" ;;
100
+ msg_add_explain3) echo "ni se imprime." ;;
101
+ msg_need_tty_secret) echo "error: secret-set requiere una terminal interactiva (stdin no es una TTY)." ;;
102
+ msg_need_tty_rotate) echo "error: rotate requiere una terminal interactiva (stdin no es una TTY)." ;;
103
+ msg_run_yourself1) echo "Corre este comando tú mismo, en una ventana de terminal real — no le hagas" ;;
104
+ msg_run_yourself2) echo "pipe de entrada ni lo corras vía un agente/herramienta de automatización." ;;
105
+ msg_run_yourself_short) echo "Corre este comando tú mismo, en una ventana de terminal real." ;;
106
+ msg_secretset_enter) echo "Ingresa el token de la API de Webflow para '%s' (entrada oculta, presiona Enter al terminar):" ;;
107
+ msg_secretset_stored) echo "Token guardado para '%s' vía %s." ;;
108
+ msg_rotate_enter) echo "Rotando el token de '%s'. Ingresa el nuevo token de la API de Webflow (entrada oculta):" ;;
109
+ msg_rotate_done) echo "Token rotado para '%s' vía %s." ;;
110
+ msg_test_hint) echo "Corre 'flowmcp test %s' para verificar que funciona." ;;
111
+ msg_test_hint_new) echo "Corre 'flowmcp test %s' para verificar que el nuevo token funciona." ;;
112
+ msg_inspect_no_session) echo "nota: aún no hay sesión guardada para '%s' — corre 'flowmcp connect %s'" ;;
113
+ msg_inspect_no_token) echo "nota: aún no hay token guardado para '%s' — corre 'flowmcp secret-set %s'" ;;
114
+ msg_invalid_org) echo "el nombre del org debe ser alfanumérico en minúsculas con - o _ (recibido '%s')" ;;
115
+ msg_org_not_found) echo "no existe el org '%s'" ;;
116
+ msg_org_exists) echo "el org '%s' ya existe (usa 'rotate' para cambiar su token, o 'remove' primero)" ;;
117
+ msg_add_ok) echo "org '%s' registrado (label: %s) · backend de secretos: %s" ;;
118
+ msg_lang_set) echo "idioma cambiado a %s" ;;
119
+ msg_connect_success) echo "'%s' conectado vía OAuth de Webflow" ;;
120
+ msg_connect_next) echo "flowmcp test %s — o 'install %s <client>' para conectarlo" ;;
121
+ msg_connect_fail) echo "no se encontró una sesión completa para '%s' — puede que el login no haya terminado" ;;
122
+ msg_connect_fail_hint) echo "corre 'flowmcp connect %s' de nuevo y espera a que muestre conectado" ;;
123
+ msg_remove_confirm_needed) echo "esto elimina el perfil y el token guardado de '%s'." ;;
124
+ msg_remove_confirm_hint) echo "vuelve a correrlo con --yes para confirmar" ;;
125
+ msg_remove_stripped) echo "se quitó 'webflow-%s' de %s" ;;
126
+ msg_remove_ok) echo "org '%s' eliminado (perfil + credenciales guardadas)" ;;
127
+ msg_install_no_org) echo "no existe el org '%s' — corre 'flowmcp add %s' o 'connect %s' primero" ;;
128
+ msg_install_ok) echo "'%s' instalado en %s" ;;
129
+ msg_install_note_mcpremote) echo "esta entrada corre mcp-remote contra el servidor MCP hosteado de Webflow — no hay ningún token en este archivo, mcp-remote lee su propia sesión aislada" ;;
130
+ msg_install_note_pat) echo "esta entrada hace referencia a run-mcp.sh, no a un token literal" ;;
131
+ msg_install_restart) echo "reinicia %s para que lo tome" ;;
132
+ msg_rename_new_exists) echo "el org '%s' ya existe — elimínalo primero o elige otro nombre" ;;
133
+ msg_rename_ok) echo "'%s' renombrado a '%s' — no hace falta volver a iniciar sesión" ;;
134
+ msg_rename_hint) echo "reinicia cualquier cliente cuyo config se haya actualizado para que tome el nuevo nombre" ;;
135
+ msg_test_mcpremote_ok) echo "'%s' tiene una sesión de Webflow guardada (mcp-remote)" ;;
136
+ msg_test_note1) echo "nota: esto solo confirma que hay una sesión guardada en tu computadora —" ;;
137
+ msg_test_note2) echo "no la prueba contra Webflow todavía. Eso pasa solo, cuando abras Claude Code," ;;
138
+ msg_test_note3) echo "Claude Desktop o Cursor: si venció, la renuevan o te piden iniciar sesión de nuevo." ;;
139
+ msg_test_pat_ok) echo "el token de '%s' es válido · sitios accesibles: %s" ;;
140
+ msg_test_scopes) echo "permisos: %s" ;;
141
+ msg_test_next_intro) echo "instálalo en el cliente que uses:" ;;
142
+ dbg_header) echo "== diagnóstico flowmcp: %s ==" ;;
143
+ dbg_done) echo "== listo ==" ;;
144
+ dbg_sec_profile) echo "perfil" ;;
145
+ dbg_sec_auth) echo "método de auth: %s" ;;
146
+ dbg_sec_mcpremote_avail) echo "disponibilidad de mcp-remote" ;;
147
+ dbg_sec_secret_backend) echo "backend de secretos: %s" ;;
148
+ dbg_sec_wfmcp_avail) echo "disponibilidad de webflow-mcp-server" ;;
149
+ dbg_sec_cred_check) echo "verificación de credenciales" ;;
150
+ dbg_sec_client_configs) echo "configs de clientes conocidos" ;;
151
+ dbg_level_fail) echo "FALLO" ;;
152
+ dbg_level_warn) echo "AVISO" ;;
153
+ dbg_level_note) echo "nota" ;;
154
+ *) wfw_t_en "$key" ;;
155
+ esac
156
+ }
157
+
158
+ wfw_t_en() {
159
+ local key="$1"
160
+ case "$key" in
161
+ tagline) echo "connects Webflow to your AI agent, without exposing your tokens" ;;
162
+ usage) echo "USAGE" ;;
163
+ onboarding) echo "ONBOARDING" ;;
164
+ daily_use) echo "DAILY USE" ;;
165
+ security_1) echo "security · this tool never accepts a token as a command-line argument and" ;;
166
+ security_2) echo "never prints one. secret-set/rotate require a real interactive TTY;" ;;
167
+ security_3) echo "connect opens a real browser (via mcp-remote, Webflow's own OAuth — no app to set up)." ;;
168
+ cmd_connect) echo "<org> [--label NAME] — add a client via browser, no setup needed (recommended)" ;;
169
+ cmd_add) echo "<org> [--label NAME] — register org metadata only" ;;
170
+ cmd_secret_set) echo "<org> — paste a token manually (headless fallback)" ;;
171
+ cmd_rotate) echo "<org> — replace a stored token" ;;
172
+ cmd_list) echo "list registered orgs + last test status" ;;
173
+ cmd_inspect) echo "<org> [--live] — show profile detail" ;;
174
+ cmd_test) echo "<org> — validate the stored credentials" ;;
175
+ cmd_install) echo "<org> <client> [--scope user|project] [--force]" ;;
176
+ cmd_install_clients) echo "client: claude-code | claude-desktop | cursor" ;;
177
+ cmd_remove) echo "<org> --yes [--from client:scope]... — delete profile + credentials" ;;
178
+ cmd_debug) echo "<org> — diagnose a broken connection" ;;
179
+ cmd_rename) echo "<old-org> <new-org> — no re-login needed" ;;
180
+ cmd_schema) echo "machine-readable command/JSON reference for agents" ;;
181
+ cmd_lang) echo "[en|es] — view or change the human-readable output language" ;;
182
+ msg_no_orgs) echo "No orgs registered yet. Run 'flowmcp add <org>' to start." ;;
183
+ msg_reconnecting) echo "reconnecting existing org '%s'..." ;;
184
+ msg_connect_opening) echo "Opening your browser for '%s' to approve access to Webflow..." ;;
185
+ msg_connect_ctrlc1) echo "Once you see it connect below, press Ctrl+C to return here — your" ;;
186
+ msg_connect_ctrlc2) echo "session is already saved to disk by that point." ;;
187
+ msg_add_recommend) echo "try 'flowmcp connect %s' first — browser login, no token to copy/paste" ;;
188
+ msg_add_no_browser) echo "No browser available? Run this yourself, in your own terminal (not through an agent):" ;;
189
+ msg_add_run_yourself) echo "run this yourself, in your own terminal (not through an agent):" ;;
190
+ msg_add_explain1) echo "It will prompt for the Webflow API token with hidden input and store it" ;;
191
+ msg_add_explain2) echo "directly in %s. The token is never passed as a command argument" ;;
192
+ msg_add_explain3) echo "and never printed." ;;
193
+ msg_need_tty_secret) echo "error: secret-set requires an interactive terminal (stdin is not a TTY)." ;;
194
+ msg_need_tty_rotate) echo "error: rotate requires an interactive terminal (stdin is not a TTY)." ;;
195
+ msg_run_yourself1) echo "Run this command yourself in a real terminal window — do not pipe input" ;;
196
+ msg_run_yourself2) echo "into it and do not run it via an agent/automation tool." ;;
197
+ msg_run_yourself_short) echo "Run this command yourself in a real terminal window." ;;
198
+ msg_secretset_enter) echo "Enter Webflow API token for '%s' (input hidden, press Enter when done):" ;;
199
+ msg_secretset_stored) echo "Stored token for '%s' via %s." ;;
200
+ msg_rotate_enter) echo "Rotating token for '%s'. Enter the new Webflow API token (input hidden):" ;;
201
+ msg_rotate_done) echo "Rotated token for '%s' via %s." ;;
202
+ msg_test_hint) echo "Run 'flowmcp test %s' to verify it works." ;;
203
+ msg_test_hint_new) echo "Run 'flowmcp test %s' to verify the new token works." ;;
204
+ msg_inspect_no_session) echo "note: no saved session yet for '%s' — run 'flowmcp connect %s'" ;;
205
+ msg_inspect_no_token) echo "note: no token stored yet for '%s' — run 'flowmcp secret-set %s'" ;;
206
+ msg_invalid_org) echo "org name must be lowercase alphanumeric with - or _ (got '%s')" ;;
207
+ msg_org_not_found) echo "no org '%s' registered" ;;
208
+ msg_org_exists) echo "org '%s' already exists (use 'rotate' to change its token, or 'remove' first)" ;;
209
+ msg_add_ok) echo "registered org '%s' (label: %s) · secret backend: %s" ;;
210
+ msg_lang_set) echo "language set to %s" ;;
211
+ msg_connect_success) echo "connected '%s' via Webflow's OAuth" ;;
212
+ msg_connect_next) echo "flowmcp test %s — or 'install %s <client>' to wire it up" ;;
213
+ msg_connect_fail) echo "no completed session found for '%s' — the login may not have finished" ;;
214
+ msg_connect_fail_hint) echo "run 'flowmcp connect %s' again and wait until it shows connected" ;;
215
+ msg_remove_confirm_needed) echo "this deletes the profile and the stored token for '%s'." ;;
216
+ msg_remove_confirm_hint) echo "re-run with --yes to confirm" ;;
217
+ msg_remove_stripped) echo "removed 'webflow-%s' from %s" ;;
218
+ msg_remove_ok) echo "removed org '%s' (profile + stored credentials)" ;;
219
+ msg_install_no_org) echo "no org '%s' registered — run 'flowmcp add %s' or 'connect %s' first" ;;
220
+ msg_install_ok) echo "installed '%s' into %s" ;;
221
+ msg_install_note_mcpremote) echo "entry runs mcp-remote against Webflow's hosted MCP server — no token in this file, mcp-remote reads its own isolated session" ;;
222
+ msg_install_note_pat) echo "entry references run-mcp.sh, not a literal token" ;;
223
+ msg_install_restart) echo "restart %s to pick it up" ;;
224
+ msg_rename_new_exists) echo "org '%s' already exists — remove it first or pick a different name" ;;
225
+ msg_rename_ok) echo "renamed '%s' to '%s' — no re-login needed" ;;
226
+ msg_rename_hint) echo "restart any client whose config was just updated to pick up the rename" ;;
227
+ msg_test_mcpremote_ok) echo "'%s' has a saved Webflow session (mcp-remote)" ;;
228
+ msg_test_note1) echo "note: this only confirms a session file exists on your computer —" ;;
229
+ msg_test_note2) echo "it doesn't check it against Webflow yet. That happens on its own when you" ;;
230
+ msg_test_note3) echo "open Claude Code, Claude Desktop, or Cursor: expired sessions refresh or re-prompt automatically." ;;
231
+ msg_test_pat_ok) echo "token for '%s' is valid · sites accessible: %s" ;;
232
+ msg_test_scopes) echo "scopes: %s" ;;
233
+ msg_test_next_intro) echo "install it into whichever client you're using:" ;;
234
+ dbg_header) echo "== flowmcp debug: %s ==" ;;
235
+ dbg_done) echo "== done ==" ;;
236
+ dbg_sec_profile) echo "profile" ;;
237
+ dbg_sec_auth) echo "auth method: %s" ;;
238
+ dbg_sec_mcpremote_avail) echo "mcp-remote availability" ;;
239
+ dbg_sec_secret_backend) echo "secret backend: %s" ;;
240
+ dbg_sec_wfmcp_avail) echo "webflow-mcp-server availability" ;;
241
+ dbg_sec_cred_check) echo "credential check" ;;
242
+ dbg_sec_client_configs) echo "known client configs" ;;
243
+ dbg_level_fail) echo "FAIL" ;;
244
+ dbg_level_warn) echo "WARN" ;;
245
+ dbg_level_note) echo "note" ;;
246
+ *) echo "$key" ;;
247
+ esac
248
+ }
package/lib/oauth.sh ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # OAuth for the mcp-remote path: Webflow hosts its own MCP server at
3
+ # $WFW_MCP_URL with an OAuth server that supports Dynamic Client
4
+ # Registration + PKCE (no client_secret, no manual app to create — verified
5
+ # against the real endpoint). We don't implement the OAuth dance ourselves;
6
+ # `npx mcp-remote` (github.com/punkpeye/mcp-remote) already does PKCE, DCR,
7
+ # browser-opening, token storage, and refresh correctly. Our job is just to
8
+ # give each org an isolated MCP_REMOTE_CONFIG_DIR so their sessions never
9
+ # collide, and to check whether that directory holds a completed login.
10
+ #
11
+ # SECURITY NOTE: mcp-remote's on-disk token storage is outside our control
12
+ # (it's a third-party tool). We don't read or touch the token files
13
+ # ourselves — only check for their existence as a signal.
14
+
15
+ # wfw_mcp_remote_connected <org> — true if a completed OAuth session
16
+ # (access/refresh tokens, not just a half-finished PKCE attempt) exists.
17
+ wfw_mcp_remote_connected() {
18
+ local dir
19
+ dir="$(wfw_mcp_remote_dir "$1")"
20
+ [[ -d "$dir" ]] || return 1
21
+ find "$dir" -type f -name '*_tokens.json' 2>/dev/null | grep -q .
22
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # Non-sensitive profile metadata (org name, backend, last test result).
3
+ # Never store token values here — see lib/secrets.sh.
4
+
5
+ wfw_profile_exists() { [[ -f "$(wfw_profile_path "$1")" ]]; }
6
+
7
+ wfw_profile_write_new() {
8
+ local org="$1" label="$2" auth_method="${3:-pat}" backend
9
+ backend="$(wfw_secret_backend)"
10
+ jq -n \
11
+ --arg org "$org" \
12
+ --arg label "$label" \
13
+ --arg created_at "$(wfw_now)" \
14
+ --arg backend "$backend" \
15
+ --arg auth_method "$auth_method" \
16
+ '{
17
+ org: $org,
18
+ label: $label,
19
+ created_at: $created_at,
20
+ secret_backend: $backend,
21
+ auth_method: $auth_method,
22
+ last_test: { timestamp: null, status: null, scopes: [], sites_count: null, error: null }
23
+ }' > "$(wfw_profile_path "$org")"
24
+ }
25
+
26
+ wfw_profile_set_auth_method() {
27
+ local org="$1" auth_method="$2" p tmp
28
+ p="$(wfw_profile_path "$org")"
29
+ tmp="$(mktemp)"
30
+ jq --arg m "$auth_method" '.auth_method = $m' "$p" > "$tmp" && mv "$tmp" "$p"
31
+ }
32
+
33
+ wfw_profile_read() { cat "$(wfw_profile_path "$1")"; }
34
+
35
+ wfw_profile_update_last_test() {
36
+ local org="$1" status="$2" scopes_json="$3" sites_count="$4" error="$5"
37
+ local p tmp
38
+ p="$(wfw_profile_path "$org")"
39
+ tmp="$(mktemp)"
40
+ jq \
41
+ --arg ts "$(wfw_now)" \
42
+ --arg status "$status" \
43
+ --argjson scopes "$scopes_json" \
44
+ --argjson sites_count "$sites_count" \
45
+ --arg error "$error" \
46
+ '.last_test = { timestamp: $ts, status: $status, scopes: $scopes, sites_count: $sites_count, error: (if $error == "" then null else $error end) }' \
47
+ "$p" > "$tmp" && mv "$tmp" "$p"
48
+ }
49
+
50
+ wfw_profile_delete() { rm -f "$(wfw_profile_path "$1")"; }
51
+
52
+ wfw_profile_list() {
53
+ shopt -s nullglob
54
+ for f in "$WFW_PROFILES_DIR"/*.json; do
55
+ basename "$f" .json
56
+ done
57
+ shopt -u nullglob
58
+ }
package/lib/secrets.sh ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # Secret storage backend abstraction: OS keychain first, chmod-600 file fallback.
3
+ #
4
+ # SECURITY CONTRACT — read before touching this file:
5
+ # - secret_get MUST NEVER be called anywhere that prints its output to a
6
+ # terminal an agent can read back (no `echo "$(secret_get ...)"`,
7
+ # no command substitution that lands in a variable that later gets
8
+ # echoed/logged, no `set -x` in a scope that calls it).
9
+ # - The only two legitimate callers of secret_get are commands/test.sh
10
+ # and commands/run-mcp.sh, and both must consume the value directly
11
+ # into a curl header or an exported env var for a child process —
12
+ # never print it.
13
+ # - secret_set MUST NEVER accept the token as a CLI argument (visible in
14
+ # `ps`, shell history, and any agent tool-call transcript). It only
15
+ # reads from a variable already held in the calling shell (which itself
16
+ # must have come from an interactive `read -s`, never from an argv).
17
+
18
+ wfw_secret_backend() {
19
+ if [[ "$(uname -s)" == "Darwin" ]] && command -v security >/dev/null 2>&1; then
20
+ echo "keychain-macos"
21
+ elif command -v secret-tool >/dev/null 2>&1; then
22
+ echo "keychain-linux"
23
+ else
24
+ echo "file"
25
+ fi
26
+ }
27
+
28
+ # secret_set <org> <token-var-name>
29
+ # Pass the *name* of the variable holding the token, not the token itself,
30
+ # so this function can unset it from the caller's scope when done.
31
+ wfw_secret_set() {
32
+ local org="$1" varname="$2" backend
33
+ backend="$(wfw_secret_backend)"
34
+ local token="${!varname}"
35
+ case "$backend" in
36
+ keychain-macos)
37
+ security add-generic-password -a "$org" -s "$WFW_KEYCHAIN_SERVICE" -w "$token" -U >/dev/null
38
+ ;;
39
+ keychain-linux)
40
+ printf '%s' "$token" | secret-tool store --label="Webflow token ($org)" \
41
+ service "$WFW_KEYCHAIN_SERVICE" account "$org"
42
+ ;;
43
+ file)
44
+ local f="$WFW_SECRETS_DIR/$org.token"
45
+ umask 077
46
+ printf '%s' "$token" > "$f"
47
+ chmod 600 "$f"
48
+ ;;
49
+ esac
50
+ unset -v "$varname"
51
+ }
52
+
53
+ # secret_get <org> -> prints token to stdout. Callers must capture, not print.
54
+ wfw_secret_get() {
55
+ local org="$1" backend
56
+ backend="$(wfw_secret_backend)"
57
+ case "$backend" in
58
+ keychain-macos)
59
+ security find-generic-password -a "$org" -s "$WFW_KEYCHAIN_SERVICE" -w 2>/dev/null
60
+ ;;
61
+ keychain-linux)
62
+ secret-tool lookup service "$WFW_KEYCHAIN_SERVICE" account "$org" 2>/dev/null
63
+ ;;
64
+ file)
65
+ cat "$WFW_SECRETS_DIR/$org.token" 2>/dev/null
66
+ ;;
67
+ esac
68
+ }
69
+
70
+ wfw_secret_exists() {
71
+ local org="$1"
72
+ [[ -n "$(wfw_secret_get "$org" || true)" ]]
73
+ }
74
+
75
+ wfw_secret_delete() {
76
+ local org="$1" backend
77
+ backend="$(wfw_secret_backend)"
78
+ case "$backend" in
79
+ keychain-macos)
80
+ security delete-generic-password -a "$org" -s "$WFW_KEYCHAIN_SERVICE" >/dev/null 2>&1 || true
81
+ ;;
82
+ keychain-linux)
83
+ secret-tool clear service "$WFW_KEYCHAIN_SERVICE" account "$org" >/dev/null 2>&1 || true
84
+ ;;
85
+ file)
86
+ rm -f "$WFW_SECRETS_DIR/$org.token"
87
+ ;;
88
+ esac
89
+ }