From 5d768c0e2d9f197048044f055b201932e7211fd9 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Thu, 23 Jul 2026 10:44:26 +0400 Subject: [PATCH 1/5] docs: turn README into first-use guide --- CHANGELOG.md | 4 + README.md | 355 ++++++++++++++++++++++---- docs/config.md | 16 +- tests/readme-product-contract.test.ts | 78 +++++- 4 files changed, 391 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f9c1d34..1ae322ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format ## [Unreleased] +### Changed + +- [#200](https://github.com/mohanagy/miftah/issues/200) Reworked the README into a task-oriented first-use guide with complete Claude Desktop setup, authentication-path selection, generic MCP onboarding, profile-management tools, native versus upstream-owned OAuth, dashboard lifecycle, everyday diagnostics/audit commands, and focused security-control guidance. + ## [0.4.0] - 2026-07-23 ### Added diff --git a/README.md b/README.md index 35409912..cf4e22d4 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,24 @@ You already have an MCP for GitHub, Sentry, PostHog, or another service. The har Miftah is a local MCP profile manager and safety layer. It wraps the upstream server you already use, gives it named profiles such as `personal`, `work`, `client-a`, or `production`, and keeps account selection intentional. +```text +Claude, Cursor, or VS Code + │ + one Miftah connector + │ + ┌────┴────┐ + work personal + │ │ + same upstream MCP +``` + Miftah runs locally by default. Miftah itself has no cloud service or telemetry; it connects only to the upstreams you configure. > **Status:** Miftah is experimental and pre-1.0. Interfaces and security behavior may change between minor versions. See the [release policy](CHANGELOG.md#release-policy) and use the [private disclosure process](SECURITY.md) for vulnerabilities. ## Why Miftah exists -Without Miftah, a multi-account setup usually becomes a growing list of client entries: `github-personal`, `github-work`, `sentry-client-a`, `sentry-client-b`, and so on. That works at first, but it makes the connection configuration—not the account context—the thing you have to manage. +Without Miftah, a multi-account setup usually becomes a growing list of client entries: `github-personal`, `github-work`, `sentry-client-a`, `sentry-client-b`, and so on. That works, but every new account duplicates launch configuration and makes the client entry—not the account context—the thing you have to manage. | Instead of this | Miftah gives you this | | --- | --- | @@ -23,93 +34,331 @@ Without Miftah, a multi-account setup usually becomes a growing list of client e Do not create one client entry for every account. Add one Miftah connector per service, then manage the accounts behind it as profiles. -## What Miftah does today - -### Keeps account context explicit - -Profiles represent real working contexts—not just different tokens. You can switch deliberately, keep a safe configured default, or add routing rules for stable signals such as a repository, organization, or project. If routing is ambiguous, Miftah does not guess. - -### Adds a control layer around existing MCPs +Miftah wraps an existing upstream MCP server. It does not replace it. The upstream still owns its provider tools and provider behavior. Miftah adds the local profile, credential, routing, policy, approval, lifecycle, redaction, and audit boundaries around it. -Miftah wraps an existing upstream MCP server. It does not replace it. Your GitHub, Sentry, PostHog, or other provider MCP continues to provide its tools and provider behavior; Miftah handles the local profile, credential, routing, policy, approval, lifecycle, and redaction boundaries around it. +## Choose your setup path -### Keeps secrets out of client configuration +Start with the row that describes how your upstream MCP authenticates. -Profiles refer to credentials from environment variables, dotenv files, OS keychains, 1Password, or an explicitly trusted local provider. Secret values do not belong in your MCP client JSON, and Miftah redacts them from its diagnostics and audit output. +| Your upstream MCP | Use this Miftah path | +| --- | --- | +| GitHub or Sentry | Generate a strict built-in preset with `miftah init`. | +| Another exact-pinned local STDIO server | Use `generic-npx` or `generic-docker`, then add profiles around it. | +| Remote HTTPS Streamable HTTP with a token or API key | Use the `streamable-http` preset with a secret-backed header. | +| Remote HTTPS Streamable HTTP with standards-compatible OAuth | Use Native remote OAuth through `miftah dashboard` or the `connection` and `auth` CLI commands. | +| Local or provider-specific MCP that opens its own OAuth flow | Use Upstream-owned OAuth. Miftah wraps the process but does not take over its token cache. | +| Google Search Console | Use the reviewed `google-search-console` adapter preset. OAuth remains upstream-owned. | +| One account and no need for profile, policy, routing, or audit controls | Keep the direct MCP entry; Miftah may not add value for this case. | -### Makes sensitive work more deliberate +Miftah requires Node.js 20 or newer. Each upstream keeps its own runtime and installation requirements; for example, the GitHub preset requires Docker and the Google Search Console adapter requires Python 3.11 or newer plus `uvx`. -Optional policies, explicit destructive-profile selection, confirmation flows, identity checks, and local audit records help keep a helpful agent from using a plausible-but-wrong account or silently proceeding with sensitive work. +## First setup: GitHub with Claude Desktop -## Get running with Claude Desktop +This path creates one Claude connector backed by two GitHub profiles: `work` and `personal`. -Install Miftah: +### 1. Install Miftah ```bash npm install -g @lubab/miftah +miftah version ``` -Generate a GitHub configuration and a Claude Desktop snippet: +### 2. Generate the Miftah configuration and client JSON ```bash miftah init github --preset github --output ~/.config/miftah/github.json --client claude-desktop ``` -The GitHub preset requires Docker and generates `GITHUB_WORK_TOKEN` and `GITHUB_PERSONAL_TOKEN` credential references. Set those references in the environment that launches Claude Desktop, then validate the configuration: +The command: + +- creates `~/.config/miftah/github.json`; +- generates the `work` and `personal` profiles; +- configures the exact-pinned GitHub MCP Docker launch; +- puts the generated profiles behind a read-only Miftah policy; and +- prints a Claude Desktop `mcpServers` snippet with absolute launcher paths. + +It does not modify Claude Desktop and does not ask for or write a token value. The complete generated configuration is checked in as the [GitHub example](examples/github.miftah.json). + +### 3. Provide the two credentials + +The generated profiles refer to `GITHUB_WORK_TOKEN` and `GITHUB_PERSONAL_TOKEN`. Supply least-privilege provider tokens through one supported secret source: + +- a GUI-visible process environment using `${ENV_NAME}` or `secretref:env://ENV_NAME`; +- a configured dotenv file; +- the OS keychain using `secretref:keychain:///`; +- 1Password using `secretref:op:////`; or +- an explicitly reviewed local secret-provider plugin. + +Claude Desktop is a GUI app and does not inherit terminal startup files such as `~/.zshrc`. A token exported only from an interactive shell can pass a terminal check and still be missing when Claude starts Miftah. Use a supported secret provider, configured dotenv file, or an environment visible to the GUI process. + +Keep raw token values out of the Miftah JSON and Claude JSON. See [secret providers](docs/config.md#secret-providers) for the exact reference grammar and prerequisites. + +### 4. Validate configuration and readiness ```bash miftah validate --config ~/.config/miftah/github.json +miftah doctor --config ~/.config/miftah/github.json +miftah test-profile --config ~/.config/miftah/github.json --profile work +miftah list-tools --config ~/.config/miftah/github.json --profile personal ``` -`miftah validate` checks the configuration; it does not start the upstream server or prove that a credential works. For a redacted readiness check after you have set the references, run: +These commands answer different questions: + +- `validate` checks the JSON and cross-references without starting the upstream. +- `doctor` resolves the required secret references and checks redacted upstream readiness. +- `test-profile` starts and initializes one exact profile. +- `list-tools` shows the tools exposed by one profile, then shuts it down. + +Do not treat a successful `validate` as proof that the credential, Docker launch, provider scopes, or account identity are correct. + +### 5. Add the generated connector to Claude Desktop + +Open Claude Desktop and use **Developer → Edit Config**. The normal locations are: + +- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +- Windows: `%APPDATA%\Claude\claude_desktop_config.json` + +Merge the generated top-level `mcpServers` property into the host file. If the file already has an `mcpServers` object, add only the generated server entry inside it; do not nest a second `mcpServers` object. + +Keep the generated `command` and `args` as JSON arrays with their absolute paths. Do not turn them into a shell command. + +Save the file and restart Claude Desktop. Miftah cannot replace an already-running client's in-memory MCP session after a configuration, dashboard, or durable profile change. + +For screenshots and host-specific notes, use the step-by-step [Claude Desktop setup](docs/claude-desktop.md). The same generator also supports Claude Code, Cursor, and VS Code; see [Preset and client compatibility](docs/presets-and-clients.md). + +### 6. Select and verify the account in Claude + +Miftah exposes profile management as MCP tools. The user or agent can call: + +| MCP management tool | Purpose | +| --- | --- | +| `miftah_list_profiles` | List configured profiles and safe binding state. | +| `miftah_current_profile` | Show the active/default profile and how it was selected. | +| `miftah_profile_info` | Inspect one profile's non-secret configuration and identity state. | +| `miftah_use_profile` | Deliberately switch to a named profile. | +| `miftah_reset_profile` | Return to the configured default profile. | +| `miftah_route_preview` | Preview profile selection and policy without performing the provider operation. | +| `miftah_health` | Inspect wrapper and upstream lifecycle health. | +| `miftah_verify_identity` | Run a configured bounded identity probe. | + +For example, ask Claude: + +```text +List the profiles available through miftah-github. +Switch miftah-github to the personal profile. +Confirm the current profile before reading my repositories. +``` + +The underlying calls are `miftah_list_profiles`, `miftah_use_profile`, and `miftah_current_profile`. If profile-switch confirmation is enabled, the client must complete that confirmation; Miftah fails closed when it cannot. + +## Add another MCP + +Built-in presets are reviewed, exact-pinned configurations. Generic compatibility means Miftah can wrap another MCP server; it does not mean that every provider has a built-in preset or that Miftah reimplements its API. + +### Built-in and generic presets + +| Preset | Intended use | +| --- | --- | +| `github` | Docker-based GitHub MCP with `work` and `personal` profiles | +| `sentry` | Exact-pinned Sentry MCP with a token reference | +| `google-search-console` | Exact-pinned `uvx` adapter with upstream-owned OAuth | +| `generic` | MCP reference/test server, not a production provider recommendation | +| `generic-npx` | Another exact-version npm MCP package | +| `generic-docker` | Another container pinned by canonical `@sha256:` digest | +| `streamable-http` | An exact HTTPS Streamable HTTP upstream | + +### Minimal exact-pinned npm MCP template + +If the upstream's documented launch is `npx --yes @` and it accepts a credential through an environment variable: ```bash -miftah doctor --config ~/.config/miftah/github.json +miftah init analytics \ + --preset generic-npx \ + --npm-package '@vendor/mcp-server@1.2.3' \ + --credential-env SERVICE_API_KEY \ + --output ~/.config/miftah/analytics.json \ + --client claude-desktop +``` + +Replace the example package, version, and variable name with the upstream's documented values. An exact version is required. Run `validate`, `doctor`, `test-profile`, and `list-tools` before adding the printed client JSON. + +If the upstream needs custom arguments, headers, working directories, several named upstreams, or profile-specific overrides, generate the nearest safe preset and then use the [Configuration reference](docs/config.md). Always keep subprocess arguments as arrays; Miftah does not need a shell command string. + +### What a multi-profile configuration contains + +The GitHub preset is a runnable two-profile example. Its central account mapping is: + +```json +{ + "defaultProfile": "work", + "profiles": { + "work": { + "description": "Work GitHub account", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_WORK_TOKEN}" + }, + "policy": "readonly" + }, + "personal": { + "description": "Personal GitHub account", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_TOKEN}" + }, + "policy": "readonly" + } + } +} ``` -Claude Desktop is a GUI app and does not inherit terminal startup files such as `~/.zshrc`; use a supported secret provider or a GUI-visible launcher environment instead of relying on shell exports alone. +This excerpt is not a complete configuration by itself. Generate or copy the complete [GitHub example](examples/github.miftah.json), which also contains the upstream, policy, routing, security, process, audit, and tooling sections. + +## OAuth and the local dashboard + +OAuth has two ownership models. Choosing the wrong one creates false expectations about where credentials live and who can refresh or revoke them. -`init --client` prints JSON with absolute launcher paths for you to copy into your client configuration. It deliberately does not modify the client file. +### Native remote OAuth -Continue with the step-by-step [Claude Desktop setup](docs/claude-desktop.md), or start from the [GitHub example](docs/examples/github.md) or [Sentry example](docs/examples/sentry.md). +Miftah can own OAuth only for an exact HTTPS Streamable HTTP MCP whose discovery metadata satisfies Miftah's standards and security checks. In that path, Miftah performs discovery, PKCE browser authorization, bounded loopback callback handling, secure OS-vault storage, refresh, and bearer injection. -Prefer a browser for OAuth setup? Run: +The easiest first run is: ```bash miftah dashboard ``` -The optional local Console opens on `127.0.0.1`, creates a validated first native-OAuth profile without hand-written JSON, shows redacted connection/audit state, and generates client JSON for you to review and copy. It stays in the foreground, never edits Claude or another client configuration, and does not accept provider passwords, browser cookies, or raw tokens. Use `--config ` to manage another Miftah configuration or `--no-open` when you only want the local URL. +The optional dashboard: + +1. starts a foreground-only service on literal `127.0.0.1`; +2. opens the system browser and asks for the one-time bootstrap code printed in the terminal; +3. creates a first validated Native remote OAuth profile and connection when the default config does not exist; +4. offers a separate **Connect** action that starts the reviewed system-browser authorization; +5. shows redacted connection and audit state; and +6. generates client JSON for you to review and copy. + +Keep the terminal process running while using the dashboard. Closing it stops the local listener. There is no daemon, hosted account, LAN mode, or automatic client-file edit. After a durable configuration or connection change, copy any regenerated client JSON and restart or reconnect the MCP client. + +Use another config path or suppress automatic browser launch with: + +```bash +miftah dashboard --config ~/.config/miftah/remote-service.json +miftah dashboard --config ~/.config/miftah/remote-service.json --no-open +``` + +The equivalent CLI path is plan-first: + +```bash +miftah connection add --config remote-service.json \ + --profile work \ + --upstream default \ + --issuer https://auth.example.com \ + --client-registration dynamic \ + --scope mcp:read + +# Review the generated oauthconn:, then repeat with: +miftah connection add --config remote-service.json \ + --profile work \ + --upstream default \ + --issuer https://auth.example.com \ + --client-registration dynamic \ + --scope mcp:read \ + --connection oauthconn: \ + --write + +miftah auth connect --config remote-service.json --connection oauthconn: +miftah connection test --config remote-service.json --connection oauthconn: +miftah connection list --config remote-service.json --client claude-desktop +``` + +Read [OAuth support](docs/oauth-support.md) before using this path. Miftah does not support OAuth for every MCP, and OAuth success alone does not prove that the token belongs to the intended account or organization. + +### Upstream-owned OAuth + +Provider-specific or local STDIO OAuth remains owned by that upstream. The upstream opens its browser flow, stores its cache, refreshes its tokens, and defines reauthentication or revocation. Miftah may wrap the process and protect its profile/policy boundary, but it does not scrape or reinterpret the upstream cache as native Miftah OAuth. + +The Google Search Console adapter is the concrete example: + +```bash +miftah init gsc \ + --preset google-search-console \ + --oauth-client-secrets-file "$HOME/.config/gsc/client-secrets.json" \ + --output "$HOME/.config/miftah/gsc.json" \ + --client claude-desktop +``` + +It runs the exact-pinned upstream through `uvx`, applies Miftah's read-only policy, and passes the configured client-secrets path to that upstream. The upstream owns the first-use browser flow and token cache. Miftah never reads, copies, exports, or deletes that cache. See the [Google Search Console provider-adapter pilot](docs/provider-adapters.md#google-search-console-pilot). + +## Everyday commands + +These are shell commands. Profile switching and identity tools such as `miftah_use_profile` are MCP management tools used from the connected client. -## A real multi-account setup +| Task | Command | +| --- | --- | +| Validate JSON only | `miftah validate --config service.json` | +| Check secrets, executable, upstream startup, discovery, and shutdown | `miftah doctor --config service.json` | +| Start one profile and verify initialization | `miftah test-profile --config service.json --profile work` | +| Discover one profile's upstream tools | `miftah list-tools --config service.json --profile work` | +| Read redacted audit events | `miftah logs --config service.json` | +| Follow redacted audit events | `miftah logs --config service.json --follow` | +| Create a redacted support snapshot | `miftah audit-export --config service.json --output support-audit.jsonl` | +| Verify configured hash-chain integrity | `miftah audit-verify --config service.json` | +| Print the JSON Schema | `miftah schema` | +| Review a supported config migration | `miftah migrate-config --config service.json` | +| Apply the reviewed migration | `miftah migrate-config --config service.json --write` | + +Use `miftah --help` or `miftah --help` for the installed version's exact grammar. The complete compatibility contract is in the [CLI reference](docs/cli.md). + +## Secrets, policy, routing, and identity -Imagine you use Sentry for two products. Instead of adding two separate Sentry servers to Claude Desktop, configure one `miftah-sentry` connector with two profiles: +### Secrets + +Supported profile values include: ```text -miftah-sentry -├── product-a -└── product-b +${ENV_NAME} +secretref:env://ENV_NAME +secretref:keychain:/// +secretref:op://// ``` -When you need the other product, select its profile deliberately. When a tool call needs it, Miftah starts or reuses the corresponding upstream context with that profile's credentials, while the client keeps one Sentry connector. The same pattern works for personal/work GitHub accounts, client environments, and staging/production services. +Dotenv paths belong in `secrets.envFiles`. Plaintext values in configuration are disabled unless the operator explicitly opts into that weaker mode. Miftah registers resolved values with its redactor before diagnostics, health, audit, or upstream errors can expose them. + +For credential files that must differ between profiles, use [profile credential isolation](docs/config.md#profile-credential-isolation). It can materialize profile-owned copies and mount only the selected profile into a supported container workflow; it is not a general same-user sandbox. + +### Policy and approvals + +Profiles can name a local policy. Policies classify operations as `read`, `write`, or `destructive`, then allow, deny, or require confirmation. A Miftah policy does not reduce provider-side token scopes: use least-privilege provider credentials as the first boundary. + +Human confirmation is the default. Delegated-agent approval is an explicit automation mode with a short-lived exact-action bearer; it is not proof that a human approved the action. See [operation routing and policy](docs/config.md#operation-routing-and-policy). + +### Routing and profile control + +Explicit profile selection is the safest answer when account identity matters. For stable context, configured rules, [provider routing matchers](docs/config.md#provider-routing-matchers), and [routing context](docs/config.md#routing-context) can select a profile from bounded repository, organization, project, URL, or client-root evidence. Ambiguous evidence fails closed. + +Use `miftah_route_preview` before a sensitive call to see the selected profile, routing reason, policy decision, risk classification, and safe evidence without forwarding the provider operation. + +When enabled, `miftah_lock_profile` and `miftah_unlock_profile` add a connection-bound runtime lock. A configured operator lock remains stronger and cannot be removed through MCP. + +### Identity + +OAuth success and valid credentials do not prove that the intended account is active. An optional bounded identity probe can bind expected provider evidence to one profile. Use `miftah_verify_identity` for a live check and inspect cached or persisted state through `miftah_profile_info`, `miftah_current_profile`, `miftah_health`, or `miftah_route_preview`. + +## Common first-run problems -## What it works with +### `DEFAULT_PROFILE_NOT_FOUND` -- **GitHub and Sentry:** strict built-in presets and generated client snippets. -- **Other compatible MCP servers:** configure a generic STDIO, Streamable HTTP, or legacy SSE upstream, then add profiles around it. -- **Multiple upstreams in one wrapper:** use an account bundle when related upstreams belong behind one controlled connection. +`defaultProfile` must exactly match a key under `profiles`. Run `miftah validate --config ` after every manual edit. -Miftah can authorize a standards-compatible remote HTTPS Streamable HTTP MCP server through discovery, PKCE, a bounded loopback browser callback, OS-vault storage, and refresh. `miftah connection …` and `miftah auth …` provide reviewed setup, status, noninteractive testing, connect, safe reauth, and local disconnect. Provider-specific or local STDIO OAuth remains owned by that upstream, and local disconnect does not claim provider-side token revocation. A bounded [Google Search Console provider-adapter pilot](docs/provider-adapters.md#google-search-console-pilot) now generates an exact-pinned upstream-owned setup without reading its token cache or pretending it is native Miftah OAuth. Read [OAuth support](docs/oauth-support.md) for the exact compatibility and configuration boundary. +### `SECRET_ENV_MISSING` even though the variable is in `.zshrc` -## Trust and control boundaries +Claude Desktop does not inherit your interactive shell startup files. Move the reference to a configured dotenv/keychain/1Password source or launch Claude from an environment that actually contains it. Confirm with `miftah doctor` before restarting Claude. -Miftah supports environment and dotenv references, plus OS keychain references such as `secretref:keychain:///` and 1Password references such as `secretref:op:////`. The reference is configuration; the secret value stays outside the MCP client configuration. +### The dashboard changed the config but Claude still uses the old profile -For credential-file workflows, see [profile credential isolation](docs/config.md#profile-credential-isolation). Where provider/account signals are stable, opt-in [provider routing matchers](docs/config.md#provider-routing-matchers) and [routing context](docs/config.md#routing-context) can inform profile selection; ambiguous context never selects an account by guesswork. +The dashboard changes durable configuration for future connections. It cannot take over an already-running Claude Desktop STDIO process or silently replace that process's in-memory selection. Restart or reconnect the MCP client. -When an operator enables profile locking, `miftah_lock_profile` and `miftah_unlock_profile` expose that control to the MCP client. For the complete security scope and future work, read the linked designs below. +### OAuth opened in the wrong place—or did not open -The optional local Console is started explicitly with `miftah dashboard`; `miftah console --config ` remains the API-only form. It binds only to loopback, uses a one-time terminal bootstrap plus browser session/CSRF protection, and exposes redacted metadata and audited connection operations under `/api/v1`. It is not a daemon and cannot change an already-running MCP client's in-memory session. See the [Console contract](docs/console-api.md). +First identify the ownership model. Native remote OAuth is started by `miftah auth connect` or the dashboard. Upstream-owned OAuth is started by that MCP server or one of its tools. Do not create a native `oauth.connections` entry for an opaque local token cache. ## Is Miftah right for you? @@ -117,20 +366,18 @@ Miftah is a good fit when account context matters: you work across clients, orga If you use one account with one direct MCP server and do not need profile, routing, policy, or audit controls, a direct MCP entry is simpler. Miftah is not an MCP marketplace and does not add provider functionality that the upstream server does not have. -## Learn more - -- [Claude Desktop setup](docs/claude-desktop.md) -- [Preset and client compatibility](docs/presets-and-clients.md) -- [GitHub example](docs/examples/github.md) and [Sentry example](docs/examples/sentry.md) -- [Configuration reference](docs/config.md) -- [Security boundary](docs/security.md), [Threat model](docs/threat-model.md), and [OAuth and Console security design](docs/oauth-console-threat-model.md) -- [OAuth support](docs/oauth-support.md) -- [Provider adapters and Google Search Console pilot](docs/provider-adapters.md) -- [Local Console dashboard and control API](docs/console-api.md) -- [CLI reference](docs/cli.md) -- [Architecture](docs/architecture.md) -- [Changelog and release policy](CHANGELOG.md) -- [Contributing](CONTRIBUTING.md) +## Continue by task + +- **Set up a client:** [Claude Desktop setup](docs/claude-desktop.md) and [Preset and client compatibility](docs/presets-and-clients.md) +- **Start from a reviewed provider:** [GitHub example](docs/examples/github.md), [Sentry example](docs/examples/sentry.md), or [Google Search Console adapter](docs/provider-adapters.md#google-search-console-pilot) +- **Configure profiles and controls:** [Configuration reference](docs/config.md) +- **Choose an authentication model:** [OAuth support](docs/oauth-support.md) +- **Use the local browser UI:** [Local Console dashboard and control API](docs/console-api.md) +- **Review trust boundaries:** [Security boundary](docs/security.md), [Threat model](docs/threat-model.md), and [OAuth and Console security design](docs/oauth-console-threat-model.md) +- **Use every command:** [CLI reference](docs/cli.md) +- **Embed Miftah as a library or plugin host:** [Public library API](docs/library-api.md) and [Plugin API](docs/plugins.md) +- **Understand internals:** [Architecture](docs/architecture.md) +- **Follow releases or contribute:** [Changelog and release policy](CHANGELOG.md) and [Contributing](CONTRIBUTING.md) ## License diff --git a/docs/config.md b/docs/config.md index eccf8b2e..f9674445 100644 --- a/docs/config.md +++ b/docs/config.md @@ -6,19 +6,21 @@ Miftah uses JSON. Generate the machine-readable schema with: miftah schema > miftah.schema.json ``` -`miftah schema` is generated from the same strict Zod contract that normalizes successful runtime configuration. Required fields are `version`, `name`, `defaultProfile`, `profiles`, and either `upstream` or `upstreams`. Version `"2"` is the canonical format written by current presets and examples. Closed configuration objects reject unknown or misspelled keys; intentionally open maps are limited to named profiles, policies, upstreams, environment/header values, tool-risk overrides, and `routing.rules[].when`. +`miftah schema` is generated from the same strict Zod contract that normalizes successful runtime configuration. Required fields are `version`, `name`, `defaultProfile`, `profiles`, and either `upstream` or `upstreams`. Version `"3"` is the canonical format written by current presets and examples. Closed configuration objects reject unknown or misspelled keys; intentionally open maps are limited to named profiles, policies, upstreams, environment/header values, tool-risk overrides, OAuth connection references, and `routing.rules[].when`. -Miftah accepts versions `"1"` and `"2"` during the documented compatibility window. It never rewrites a file while loading or serving it. An unsupported version returns `UNSUPPORTED_CONFIG_VERSION` with remediation. +Miftah accepts versions `"1"`, `"2"`, and `"3"` during the documented compatibility window. It never rewrites a file while loading or serving it. An unsupported version returns `UNSUPPORTED_CONFIG_VERSION` with remediation. ## Configuration version compatibility and migration -Version `"1"` remains supported for existing configurations. It allows the historical `upstream.transport: "http"` alias, `security.allowPlaintextSecrets`, and redundant true-only `security.redactSecrets` / `audit.redact` declarations. Version `"2"` rejects those aliases with `UNSUPPORTED_CONFIG_OPTION`: use `upstream.transport: "streamable-http"`, `secrets.allowPlaintextSecrets`, and omit force-on redaction declarations. +Version `"1"` remains supported for existing configurations. It allows the historical `upstream.transport: "http"` alias, `security.allowPlaintextSecrets`, and redundant true-only `security.redactSecrets` / `audit.redact` declarations. Versions `"2"` and `"3"` reject those aliases with `UNSUPPORTED_CONFIG_OPTION`: use `upstream.transport: "streamable-http"`, `secrets.allowPlaintextSecrets`, and omit force-on redaction declarations. -Run `miftah migrate-config --config ` to inspect a version-1 upgrade. The default is a JSON migration plan only; it does not alter the source file, resolve secrets, or start an upstream. Add `--write` only after reviewing that plan. Before it changes a file, Miftah requires valid UTF-8 JSON, validates the version-2 candidate, captures a regular non-symlink source snapshot, and moves the verified source into a dedicated same-directory transaction directory. It creates the exact-byte backup and synced candidate privately, then publishes each only to an absent destination path; it never uses an overwrite rename. A concurrent file is preserved, while uncertain original state is retained in the transaction directory whose path is reported in the error. Miftah restores the verified original when it can do so without overwriting anything. It preserves POSIX source mode where supported. On Windows, the transaction directory receives a current-user-only DACL at creation, and Miftah copies and verifies the source owner, group, and DACL before either private file receives source-derived bytes; inability to establish that boundary fails closed. It refuses to overwrite an existing backup and never prints a raw configuration or a configuration diff. +Version `"2"` is the prior canonical non-OAuth format. Version `"3"` keeps its existing configuration surface and adds the optional non-secret `oauth.connections` map. OAuth credentials, authorization codes, callbacks, and tokens are never configuration fields. -The migrator changes only aliases with proven equivalent behavior: `"http"` becomes `"streamable-http"`, the plaintext opt-in moves to `secrets.allowPlaintextSecrets`, and redundant force-on redaction declarations are removed. It fails closed for conflicting plaintext options or unrelated unsupported fields rather than discarding them. A version-2 input is validated and reported as unchanged; `--write` then creates no backup and performs no write. +`migrate-config` supports v1/v2 input and v3 output. Run `miftah migrate-config --config ` to inspect the upgrade. The default is a JSON migration plan only; it does not alter the source file, resolve secrets, synthesize an OAuth connection, or start an upstream. Add `--write` only after reviewing that plan. Before it changes a file, Miftah requires valid UTF-8 JSON, validates the version-3 candidate, captures a regular non-symlink source snapshot, and moves the verified source into a dedicated same-directory transaction directory. It creates the exact-byte backup and synced candidate privately, then publishes each only to an absent destination path; it never uses an overwrite rename. A concurrent file is preserved, while uncertain original state is retained in the transaction directory whose path is reported in the error. Miftah restores the verified original when it can do so without overwriting anything. It preserves POSIX source mode where supported. On Windows, the transaction directory receives a current-user-only DACL at creation, and Miftah copies and verifies the source owner, group, and DACL before either private file receives source-derived bytes; inability to establish that boundary fails closed. It refuses to overwrite an existing backup and never prints a raw configuration or a configuration diff. -Version `"1"` will remain accepted through at least the first published pre-1.0 minor release after version `"2"` ships. Removing it requires a later minor release, an **Unreleased** changelog entry, and a documented explicit migration path. Keep a byte-for-byte historical fixture for every supported released format in release CI. +For version 1, the migrator changes only aliases with proven equivalent behavior: `"http"` becomes `"streamable-http"`, the plaintext opt-in moves to `secrets.allowPlaintextSecrets`, and redundant force-on redaction declarations are removed. It fails closed for conflicting plaintext options or unrelated unsupported fields rather than discarding them. A version-2 input preserves the existing configuration while changing only the format version; it never infers native OAuth from a static `Authorization` header. A version-3 input is validated and reported as unchanged; `--write` then creates no backup and performs no write. + +Removing version 1 or 2 support requires a later minor release, an **Unreleased** changelog entry, and a documented explicit migration path. Keep a byte-for-byte historical fixture for every supported released format in release CI. For strict starter configurations, use the versioned `init` catalog rather than treating generic command examples as trusted upstream recommendations. The [preset and client compatibility matrix](presets-and-clients.md) records exact pins, required inputs, upstream provenance, and the validation boundary for every catalog entry. @@ -448,4 +450,4 @@ A profile `lease` is optional and has `ttlMs` from 1,000 through 3,600,000 milli `startMode` and `cache` remain unsupported because Miftah currently always lazily creates cached sessions. They are rejected rather than silently ignored. -Secret and audit redaction are force-on protections. Version `"1"` accepts `security.redactSecrets` and `audit.redact` only when they are `true` for compatibility; version `"2"` rejects both redundant declarations. Setting either to `false` is rejected in every supported version. Audit output is always JSONL; `audit.format` therefore accepts only `"jsonl"`. +Secret and audit redaction are force-on protections. Version `"1"` accepts `security.redactSecrets` and `audit.redact` only when they are `true` for compatibility; versions `"2"` and `"3"` reject both redundant declarations. Setting either to `false` is rejected in every supported version. Audit output is always JSONL; `audit.format` therefore accepts only `"jsonl"`. diff --git a/tests/readme-product-contract.test.ts b/tests/readme-product-contract.test.ts index e821b052..b5551425 100644 --- a/tests/readme-product-contract.test.ts +++ b/tests/readme-product-contract.test.ts @@ -1,8 +1,26 @@ -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8"); +function headingSlugs(markdown: string): Set { + const slugs = new Set(); + const duplicates = new Map(); + for (const match of markdown.matchAll(/^#{1,6}\s+(.+?)\s*$/gmu)) { + const base = match[1]! + .toLowerCase() + .replace(/[`*_~]/gu, "") + .replace(/[^\p{Letter}\p{Number}\s-]/gu, "") + .trim() + .replace(/\s+/gu, "-") + .replace(/-+/gu, "-"); + const count = duplicates.get(base) ?? 0; + duplicates.set(base, count + 1); + slugs.add(count === 0 ? base : `${base}-${count}`); + } + return slugs; +} + describe("product README", () => { it("leads with the multi-account outcome instead of internal implementation terms", () => { expect(readme).toContain("## One MCP connector. Deliberate account selection."); @@ -29,4 +47,62 @@ describe("product README", () => { expect(readme).toContain("[Configuration reference](docs/config.md)"); expect(readme).toContain("[Security boundary](docs/security.md)"); }); + + it("gives a first-time user complete setup and profile-selection journeys", () => { + expect(readme).toContain("## Choose your setup path"); + expect(readme).toContain("## First setup: GitHub with Claude Desktop"); + expect(readme).toContain("Developer → Edit Config"); + expect(readme).toContain("restart Claude Desktop"); + expect(readme).toContain("miftah test-profile --config"); + expect(readme).toContain("miftah list-tools --config"); + expect(readme).toContain("`miftah_list_profiles`"); + expect(readme).toContain("`miftah_current_profile`"); + expect(readme).toContain("`miftah_use_profile`"); + expect(readme).toContain("`miftah_reset_profile`"); + }); + + it("separates generic MCP, native OAuth, and upstream-owned OAuth onboarding", () => { + expect(readme).toContain("## Add another MCP"); + expect(readme).toContain("--preset generic-npx"); + expect(readme).toContain("--npm-package"); + expect(readme).toContain("## OAuth and the local dashboard"); + expect(readme).toContain("Native remote OAuth"); + expect(readme).toContain("Upstream-owned OAuth"); + expect(readme).toContain("miftah connection add --config"); + expect(readme).toContain("miftah auth connect --config"); + expect(readme).toContain("--preset google-search-console"); + }); + + it("makes everyday safety and operational features discoverable", () => { + expect(readme).toContain("## Everyday commands"); + expect(readme).toContain("miftah logs --config"); + expect(readme).toContain("miftah audit-export --config"); + expect(readme).toContain("miftah audit-verify --config"); + expect(readme).toContain("## Secrets, policy, routing, and identity"); + expect(readme).toContain("`${ENV_NAME}`"); + expect(readme).toContain("`secretref:env://ENV_NAME`"); + expect(readme).toContain("`miftah_route_preview`"); + expect(readme).toContain("`miftah_verify_identity`"); + }); + + it("keeps every local README link and heading anchor resolvable", () => { + const links = [...readme.matchAll(/\[[^\]]+\]\(([^)]+)\)/gu)].map((match) => match[1]!); + for (const link of links) { + if (/^(?:https?:|mailto:)/u.test(link)) continue; + const [path, fragment] = link.split("#", 2); + const target = new URL(path === "" ? "../README.md" : `../${path}`, import.meta.url); + expect(existsSync(target), `missing README link target: ${link}`).toBe(true); + if (fragment !== undefined && fragment.length > 0) { + const targetMarkdown = readFileSync(target, "utf8"); + expect(headingSlugs(targetMarkdown), `missing README anchor: ${link}`).toContain(fragment); + } + } + }); + + it("routes readers to a configuration guide that identifies v3 as the current format", () => { + const config = readFileSync(new URL("../docs/config.md", import.meta.url), "utf8"); + expect(config).toContain('Version `"3"` is the canonical format written by current presets and examples.'); + expect(config).toContain('Miftah accepts versions `"1"`, `"2"`, and `"3"`'); + expect(config).toContain("`migrate-config` supports v1/v2 input and v3 output"); + }); }); From 81f9e2b72f410c9f4ce410a8524d3e95aa3f8b5e Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Thu, 23 Jul 2026 11:38:40 +0400 Subject: [PATCH 2/5] docs: correct first-use guide contracts --- CHANGELOG.md | 2 +- README.md | 32 ++++--- docs/config.md | 11 ++- tests/readme-product-contract.test.ts | 116 ++++++++++++++++++++++++-- 4 files changed, 136 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae322ba..5ada54b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. The format ### Changed -- [#200](https://github.com/mohanagy/miftah/issues/200) Reworked the README into a task-oriented first-use guide with complete Claude Desktop setup, authentication-path selection, generic MCP onboarding, profile-management tools, native versus upstream-owned OAuth, dashboard lifecycle, everyday diagnostics/audit commands, and focused security-control guidance. +- [#200](https://github.com/mohanagy/miftah/issues/200) Reworked the README into a task-oriented first-use guide with complete Claude Desktop setup, authentication-path selection, generic MCP onboarding, profile-management tools, native versus upstream-owned OAuth, dashboard lifecycle, everyday diagnostics/audit commands, and focused security-control guidance; corrected stale configuration-version guidance to identify v3 as current while preserving an explicit v1/v2 removal window. ## [0.4.0] - 2026-07-23 diff --git a/README.md b/README.md index cf4e22d4..ea8ef9de 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ Start with the row that describes how your upstream MCP authenticates. Miftah requires Node.js 20 or newer. Each upstream keeps its own runtime and installation requirements; for example, the GitHub preset requires Docker and the Google Search Console adapter requires Python 3.11 or newer plus `uvx`. +Shell examples below use POSIX syntax, including `~`, `$HOME`, and `\` line continuations. On Windows, run the same Miftah options from PowerShell with Windows paths and PowerShell line continuation, or put the command on one line. + ## First setup: GitHub with Claude Desktop This path creates one Claude connector backed by two GitHub profiles: `work` and `personal`. @@ -120,11 +122,11 @@ Open Claude Desktop and use **Developer → Edit Config**. The normal locations Merge the generated top-level `mcpServers` property into the host file. If the file already has an `mcpServers` object, add only the generated server entry inside it; do not nest a second `mcpServers` object. -Keep the generated `command` and `args` as JSON arrays with their absolute paths. Do not turn them into a shell command. +Keep the generated `command` as a string and `args` as an array, with the absolute paths exactly as printed. Do not turn them into a shell command. Save the file and restart Claude Desktop. Miftah cannot replace an already-running client's in-memory MCP session after a configuration, dashboard, or durable profile change. -For screenshots and host-specific notes, use the step-by-step [Claude Desktop setup](docs/claude-desktop.md). The same generator also supports Claude Code, Cursor, and VS Code; see [Preset and client compatibility](docs/presets-and-clients.md). +For host-specific notes, use the [Claude Desktop setup](docs/claude-desktop.md). The same generator also supports Claude Code, Cursor, and VS Code; see [Preset and client compatibility](docs/presets-and-clients.md). ### 6. Select and verify the account in Claude @@ -144,12 +146,12 @@ Miftah exposes profile management as MCP tools. The user or agent can call: For example, ask Claude: ```text -List the profiles available through miftah-github. -Switch miftah-github to the personal profile. +List the profiles available through `github`. +Switch `github` to the personal profile. Confirm the current profile before reading my repositories. ``` -The underlying calls are `miftah_list_profiles`, `miftah_use_profile`, and `miftah_current_profile`. If profile-switch confirmation is enabled, the client must complete that confirmation; Miftah fails closed when it cannot. +The underlying calls are `miftah_list_profiles`, `miftah_use_profile`, and `miftah_current_profile`. The generated GitHub preset requires confirmation for every profile switch. With the default human approval mode, the client must support MCP form elicitation; a client without it cannot switch and Miftah fails closed. Use a form-capable client, or explicitly configure delegated-agent approval only after reviewing the [profile confirmation trade-off](docs/config.md#profile-confirmation-locks-and-leases). ## Add another MCP @@ -226,11 +228,13 @@ The easiest first run is: miftah dashboard ``` +`miftah dashboard` uses `~/.config/miftah/miftah.json` by default. That is separate from the `github.json` created earlier. Pass `--config` when you intend to open another Miftah configuration. First-run onboarding is available only when the selected target file does not exist; the dashboard never overwrites an existing file. + The optional dashboard: 1. starts a foreground-only service on literal `127.0.0.1`; 2. opens the system browser and asks for the one-time bootstrap code printed in the terminal; -3. creates a first validated Native remote OAuth profile and connection when the default config does not exist; +3. creates a first validated Native remote OAuth profile and connection when the selected config path does not exist; 4. offers a separate **Connect** action that starts the reviewed system-browser authorization; 5. shows redacted connection and audit state; and 6. generates client JSON for you to review and copy. @@ -247,16 +251,18 @@ miftah dashboard --config ~/.config/miftah/remote-service.json --no-open The equivalent CLI path is plan-first: ```bash -miftah connection add --config remote-service.json \ - --profile work \ +miftah init remote-service --preset streamable-http --url https://mcp.example.com --output ~/.config/miftah/remote-service.json + +miftah connection add --config ~/.config/miftah/remote-service.json \ + --profile default \ --upstream default \ --issuer https://auth.example.com \ --client-registration dynamic \ --scope mcp:read # Review the generated oauthconn:, then repeat with: -miftah connection add --config remote-service.json \ - --profile work \ +miftah connection add --config ~/.config/miftah/remote-service.json \ + --profile default \ --upstream default \ --issuer https://auth.example.com \ --client-registration dynamic \ @@ -264,9 +270,9 @@ miftah connection add --config remote-service.json \ --connection oauthconn: \ --write -miftah auth connect --config remote-service.json --connection oauthconn: -miftah connection test --config remote-service.json --connection oauthconn: -miftah connection list --config remote-service.json --client claude-desktop +miftah auth connect --config ~/.config/miftah/remote-service.json --connection oauthconn: +miftah connection test --config ~/.config/miftah/remote-service.json --connection oauthconn: +miftah connection list --config ~/.config/miftah/remote-service.json --client claude-desktop ``` Read [OAuth support](docs/oauth-support.md) before using this path. Miftah does not support OAuth for every MCP, and OAuth success alone does not prove that the token belongs to the intended account or organization. diff --git a/docs/config.md b/docs/config.md index f9674445..2f718383 100644 --- a/docs/config.md +++ b/docs/config.md @@ -20,7 +20,7 @@ Version `"2"` is the prior canonical non-OAuth format. Version `"3"` keeps its e For version 1, the migrator changes only aliases with proven equivalent behavior: `"http"` becomes `"streamable-http"`, the plaintext opt-in moves to `secrets.allowPlaintextSecrets`, and redundant force-on redaction declarations are removed. It fails closed for conflicting plaintext options or unrelated unsupported fields rather than discarding them. A version-2 input preserves the existing configuration while changing only the format version; it never infers native OAuth from a static `Authorization` header. A version-3 input is validated and reported as unchanged; `--write` then creates no backup and performs no write. -Removing version 1 or 2 support requires a later minor release, an **Unreleased** changelog entry, and a documented explicit migration path. Keep a byte-for-byte historical fixture for every supported released format in release CI. +Versions 1 and 2 will remain accepted through at least the first published pre-1.0 minor release after version 3 ships. Removing either version requires a later minor release, an **Unreleased** changelog entry, and a documented explicit migration path. Keep a byte-for-byte historical fixture for every supported released format in release CI. For strict starter configurations, use the versioned `init` catalog rather than treating generic command examples as trusted upstream recommendations. The [preset and client compatibility matrix](presets-and-clients.md) records exact pins, required inputs, upstream provenance, and the validation boundary for every catalog entry. @@ -61,14 +61,17 @@ Profile `env` values can reference `${ENV_NAME}` or `secretref:env://ENV_NAME`. `providerTimeoutMs` is an optional deadline for one external provider command. It is an integer from **100 ms** through **120,000 ms** and defaults to **10 seconds**. The deadline includes provider launcher startup, lookup, and cleanup. Cancellation returns a stable secret-provider cancellation error; a deadline returns a timeout error. Miftah never retries an external lookup automatically. -The supported external reference forms are exactly: +The built-in secret sources and reference forms are: -| Provider | Reference | Lookup | +| Provider | Reference or configuration | Lookup | | --- | --- | --- | +| Process environment | `${ENV_NAME}` or `secretref:env://ENV_NAME` | Exact inherited environment variable | +| Dotenv | `secretref:dotenv://` with `secrets.envFiles` | Exact name from ordered configuration-relative dotenv files | | OS keychain | `secretref:keychain:///` | macOS Keychain, Linux Secret Service, or Windows Credential Manager | | 1Password CLI | `secretref:op:////` | `op read --no-newline op:////` | +| Explicit plaintext opt-in | `secretref:plain://` | Literal value only when `secrets.allowPlaintextSecrets` is `true` | -Each component is percent-decoded exactly once and must be nonempty, at most 255 characters, well-formed Unicode, and free of controls, dot segments, `@`, `?`, `#`, `/`, and `\\`. Use percent encoding for spaces or literal percent signs. Plaintext references remain opt-in only and never place their payload in a diagnostic. +For keychain and 1Password references, each path component is percent-decoded exactly once and must be nonempty, at most 255 characters, well-formed Unicode, and free of controls, dot segments, `@`, `?`, `#`, `/`, and `\\`. Use percent encoding for spaces or literal percent signs. Environment and dotenv references use exact variable names. Plaintext references remain opt-in only and never place their payload in a diagnostic. On macOS Miftah runs the fixed `/usr/bin/security find-generic-password -s -a -w` form. On Linux it resolves `secret-tool` from an absolute `PATH` entry and runs `secret-tool lookup service account `. On Windows it reads the generic credential named `miftah:keychain::` through a fixed Credential Manager helper. Miftah never executes these commands through a shell. diff --git a/tests/readme-product-contract.test.ts b/tests/readme-product-contract.test.ts index b5551425..d95684e6 100644 --- a/tests/readme-product-contract.test.ts +++ b/tests/readme-product-contract.test.ts @@ -1,7 +1,12 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; import { describe, expect, it } from "vitest"; +import { CLI_COMMANDS, renderCommandHelp, type CliCommand } from "../src/cli/parse.js"; +import { buildPresetConfig, PRESET_CATALOG } from "../src/config/presets.js"; +import { CURRENT_CONFIG_VERSION, SUPPORTED_CONFIG_VERSIONS } from "../src/config/versions.js"; +import { MANAGEMENT_TOOL_NAMES } from "../src/mcp/server/management-tools.js"; const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8"); +const changelog = readFileSync(new URL("../CHANGELOG.md", import.meta.url), "utf8"); function headingSlugs(markdown: string): Set { const slugs = new Set(); @@ -9,11 +14,10 @@ function headingSlugs(markdown: string): Set { for (const match of markdown.matchAll(/^#{1,6}\s+(.+?)\s*$/gmu)) { const base = match[1]! .toLowerCase() - .replace(/[`*_~]/gu, "") - .replace(/[^\p{Letter}\p{Number}\s-]/gu, "") + .replace(/[`*~]/gu, "") + .replace(/[^\p{Letter}\p{Number}\s_-]/gu, "") .trim() - .replace(/\s+/gu, "-") - .replace(/-+/gu, "-"); + .replace(/\s+/gu, "-"); const count = duplicates.get(base) ?? 0; duplicates.set(base, count + 1); slugs.add(count === 0 ? base : `${base}-${count}`); @@ -59,6 +63,11 @@ describe("product README", () => { expect(readme).toContain("`miftah_current_profile`"); expect(readme).toContain("`miftah_use_profile`"); expect(readme).toContain("`miftah_reset_profile`"); + expect(readme).toContain("through `github`"); + expect(readme).not.toContain("through miftah-github"); + expect(readme).toContain("The generated GitHub preset requires confirmation for every profile switch"); + expect(readme).toContain("form elicitation"); + expect(readme).toContain("`command` as a string and `args` as an array"); }); it("separates generic MCP, native OAuth, and upstream-owned OAuth onboarding", () => { @@ -71,6 +80,18 @@ describe("product README", () => { expect(readme).toContain("miftah connection add --config"); expect(readme).toContain("miftah auth connect --config"); expect(readme).toContain("--preset google-search-console"); + expect(readme).toContain( + "miftah init remote-service --preset streamable-http --url https://mcp.example.com --output ~/.config/miftah/remote-service.json" + ); + expect(readme).toMatch( + /miftah connection add --config ~\/\.config\/miftah\/remote-service\.json \\\n\s+--profile default/gu + ); + expect(readme).toContain("uses `~/.config/miftah/miftah.json` by default"); + }); + + it("describes the Claude Desktop guide without promising missing screenshots", () => { + expect(readme).toContain("For host-specific notes, use the [Claude Desktop setup]"); + expect(readme).not.toContain("For screenshots and host-specific notes"); }); it("makes everyday safety and operational features discoverable", () => { @@ -83,6 +104,7 @@ describe("product README", () => { expect(readme).toContain("`secretref:env://ENV_NAME`"); expect(readme).toContain("`miftah_route_preview`"); expect(readme).toContain("`miftah_verify_identity`"); + expect(readme).toContain("Shell examples below use POSIX syntax"); }); it("keeps every local README link and heading anchor resolvable", () => { @@ -92,6 +114,7 @@ describe("product README", () => { const [path, fragment] = link.split("#", 2); const target = new URL(path === "" ? "../README.md" : `../${path}`, import.meta.url); expect(existsSync(target), `missing README link target: ${link}`).toBe(true); + expect(statSync(target).isFile(), `README link target is not a file: ${link}`).toBe(true); if (fragment !== undefined && fragment.length > 0) { const targetMarkdown = readFileSync(target, "utf8"); expect(headingSlugs(targetMarkdown), `missing README anchor: ${link}`).toContain(fragment); @@ -99,10 +122,89 @@ describe("product README", () => { } }); + it("models GitHub heading anchors with underscores", () => { + expect(headingSlugs("### `DEFAULT_PROFILE_NOT_FOUND`")).toContain("default_profile_not_found"); + }); + + it("models GitHub heading anchors without collapsing repeated hyphens", () => { + expect(headingSlugs("## Foo - Bar")).toContain("foo---bar"); + }); + + it("binds documented preset and management-tool names to production catalogs", () => { + const presetNames = [...readme.matchAll(/--preset\s+([a-z0-9-]+)/gu)].map((match) => match[1]!); + for (const preset of presetNames) { + expect(Object.keys(PRESET_CATALOG.presets), `unknown README preset: ${preset}`).toContain(preset); + } + + const managementNames = new Set(readme.match(/\bmiftah_[a-z0-9_]+\b/gu) ?? []); + for (const managementName of managementNames) { + expect(MANAGEMENT_TOOL_NAMES, `unknown README management tool: ${managementName}`).toContain(managementName); + } + }); + + it("binds fenced and inline shell commands and flags to the production CLI contract", () => { + const bashBlocks = [...readme.matchAll(/```bash\n([\s\S]*?)```/gu)].map((match) => match[1]!); + const fencedLines = bashBlocks.flatMap((block) => + block + .replace(/\\\n\s*/gu, " ") + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.startsWith("miftah ")) + ); + const inlineLines = [...readme.matchAll(/`(miftah [^`\n]+)`/gu)] + .map((match) => match[1]!) + .filter((line) => !line.includes("") && line !== "miftah --help"); + const logicalLines = [...new Set([...fencedLines, ...inlineLines])]; + + for (const line of logicalLines) { + const match = /^miftah\s+([a-z][a-z-]*)(?:\s+([a-z][a-z-]*))?/u.exec(line); + expect(match, `could not parse README command: ${line}`).not.toBeNull(); + const primary = match![1]!; + const command = (primary === "connection" || primary === "auth" ? `${primary} ${match![2]}` : primary) as CliCommand; + expect(Object.keys(CLI_COMMANDS), `unknown README command: ${command}`).toContain(command); + const help = renderCommandHelp(command); + const supportedFlags = new Set(help.match(/--[a-z][a-z-]*/gu) ?? []); + for (const flag of line.match(/--[a-z][a-z-]*/gu) ?? []) { + expect(supportedFlags, `unsupported README flag for ${command}: ${flag}`).toContain(flag); + } + } + }); + + it("uses a profile created by the documented native OAuth preset", () => { + const profile = /miftah connection add --config ~\/\.config\/miftah\/remote-service\.json \\\n\s+--profile ([a-z0-9-]+)/u.exec( + readme + )?.[1]; + expect(profile, "missing native OAuth profile").toBeDefined(); + const config = buildPresetConfig("remote-service", "streamable-http", { + url: "https://mcp.example.com" + }); + expect(Object.keys(config.profiles), `unknown native OAuth profile: ${profile}`).toContain(profile); + }); + it("routes readers to a configuration guide that identifies v3 as the current format", () => { const config = readFileSync(new URL("../docs/config.md", import.meta.url), "utf8"); - expect(config).toContain('Version `"3"` is the canonical format written by current presets and examples.'); - expect(config).toContain('Miftah accepts versions `"1"`, `"2"`, and `"3"`'); + const supportedVersions = SUPPORTED_CONFIG_VERSIONS.map((version) => `\`"${version}"\``); + const supportedVersionList = + supportedVersions.length === 1 + ? supportedVersions[0] + : `${supportedVersions.slice(0, -1).join(", ")}, and ${supportedVersions.at(-1)}`; + expect(config).toContain( + `Version \`"${CURRENT_CONFIG_VERSION}"\` is the canonical format written by current presets and examples.` + ); + expect(config).toContain(`Miftah accepts versions ${supportedVersionList}`); expect(config).toContain("`migrate-config` supports v1/v2 input and v3 output"); }); + + it("documents the complete built-in secret grammar at the linked target", () => { + const config = readFileSync(new URL("../docs/config.md", import.meta.url), "utf8"); + expect(config).toContain("| Process environment | `${ENV_NAME}` or `secretref:env://ENV_NAME`"); + expect(config).toContain("| Dotenv | `secretref:dotenv://` with `secrets.envFiles`"); + expect(config).toContain("| Explicit plaintext opt-in | `secretref:plain://`"); + expect(config).toContain("For keychain and 1Password references, each path component"); + }); + + it("records both the onboarding rewrite and corrected version guidance", () => { + expect(changelog).toContain("Reworked the README into a task-oriented first-use guide"); + expect(changelog).toContain("corrected stale configuration-version guidance"); + }); }); From 47edc75d5711a3deb0e3d2ad64d6e54dd8efe4e9 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Thu, 23 Jul 2026 11:43:19 +0400 Subject: [PATCH 3/5] ci: retrigger README current-head checks From b4215b7a409d1dcc9341a05f78bc9d919bb03c8f Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Thu, 23 Jul 2026 11:48:48 +0400 Subject: [PATCH 4/5] docs: explain native OAuth default profile --- README.md | 4 ++++ tests/readme-product-contract.test.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index ea8ef9de..490c3dbf 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,11 @@ The equivalent CLI path is plan-first: ```bash miftah init remote-service --preset streamable-http --url https://mcp.example.com --output ~/.config/miftah/remote-service.json +``` + +The `streamable-http` preset creates one profile named `default`, so use that exact profile when registering the connection: +```bash miftah connection add --config ~/.config/miftah/remote-service.json \ --profile default \ --upstream default \ diff --git a/tests/readme-product-contract.test.ts b/tests/readme-product-contract.test.ts index d95684e6..2eb64432 100644 --- a/tests/readme-product-contract.test.ts +++ b/tests/readme-product-contract.test.ts @@ -86,6 +86,7 @@ describe("product README", () => { expect(readme).toMatch( /miftah connection add --config ~\/\.config\/miftah\/remote-service\.json \\\n\s+--profile default/gu ); + expect(readme).toContain("The `streamable-http` preset creates one profile named `default`"); expect(readme).toContain("uses `~/.config/miftah/miftah.json` by default"); }); From 3658093b57f983f6293e8fdec9b670c8a1415138 Mon Sep 17 00:00:00 2001 From: mohammed naji Date: Thu, 23 Jul 2026 11:56:02 +0400 Subject: [PATCH 5/5] docs: make troubleshooting commands pasteable --- README.md | 10 +++++----- tests/readme-product-contract.test.ts | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 490c3dbf..847846df 100644 --- a/README.md +++ b/README.md @@ -264,18 +264,18 @@ miftah connection add --config ~/.config/miftah/remote-service.json \ --client-registration dynamic \ --scope mcp:read -# Review the generated oauthconn:, then repeat with: +# Review the generated oauthconn: UUID, then replace UUID_FROM_PLAN below: miftah connection add --config ~/.config/miftah/remote-service.json \ --profile default \ --upstream default \ --issuer https://auth.example.com \ --client-registration dynamic \ --scope mcp:read \ - --connection oauthconn: \ + --connection oauthconn:UUID_FROM_PLAN \ --write -miftah auth connect --config ~/.config/miftah/remote-service.json --connection oauthconn: -miftah connection test --config ~/.config/miftah/remote-service.json --connection oauthconn: +miftah auth connect --config ~/.config/miftah/remote-service.json --connection oauthconn:UUID_FROM_PLAN +miftah connection test --config ~/.config/miftah/remote-service.json --connection oauthconn:UUID_FROM_PLAN miftah connection list --config ~/.config/miftah/remote-service.json --client claude-desktop ``` @@ -360,7 +360,7 @@ OAuth success and valid credentials do not prove that the intended account is ac ### `SECRET_ENV_MISSING` even though the variable is in `.zshrc` -Claude Desktop does not inherit your interactive shell startup files. Move the reference to a configured dotenv/keychain/1Password source or launch Claude from an environment that actually contains it. Confirm with `miftah doctor` before restarting Claude. +Claude Desktop does not inherit your interactive shell startup files. Move the reference to a configured dotenv/keychain/1Password source or launch Claude from an environment that actually contains it. Confirm with `miftah doctor --config ~/.config/miftah/github.json` before restarting Claude. ### The dashboard changed the config but Claude still uses the old profile diff --git a/tests/readme-product-contract.test.ts b/tests/readme-product-contract.test.ts index 2eb64432..226f938b 100644 --- a/tests/readme-product-contract.test.ts +++ b/tests/readme-product-contract.test.ts @@ -87,6 +87,8 @@ describe("product README", () => { /miftah connection add --config ~\/\.config\/miftah\/remote-service\.json \\\n\s+--profile default/gu ); expect(readme).toContain("The `streamable-http` preset creates one profile named `default`"); + expect(readme).toContain("oauthconn:UUID_FROM_PLAN"); + expect(readme).not.toContain("oauthconn:"); expect(readme).toContain("uses `~/.config/miftah/miftah.json` by default"); }); @@ -106,6 +108,7 @@ describe("product README", () => { expect(readme).toContain("`miftah_route_preview`"); expect(readme).toContain("`miftah_verify_identity`"); expect(readme).toContain("Shell examples below use POSIX syntax"); + expect(readme).toContain("Confirm with `miftah doctor --config ~/.config/miftah/github.json`"); }); it("keeps every local README link and heading anchor resolvable", () => {