Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ Run a wrapped server directly when testing local STDIO:
miftah --config ~/.config/miftah/github.json
```

To serve a local Streamable HTTP endpoint instead, configure `server.http` and run:

```bash
miftah serve --transport http --config ~/.config/miftah/github.json
```

The default endpoint is `http://127.0.0.1:3000/mcp`; see [HTTP server transport](docs/config.md#http-server-transport) before exposing any non-loopback address.

## Profiles

Profiles are named credential environments. Keep secret values outside JSON and use the exact generated references in the checked-in [GitHub](examples/github.miftah.json), [Sentry](examples/sentry.miftah.json), or [generic reference](examples/generic.miftah.json) example. The strict catalog pins GitHub to `ghcr.io/github/github-mcp-server:v1.5.0` with its documented read-only tool configuration; it does not claim a digest. The [compatibility matrix](docs/presets-and-clients.md) describes safe promotion and deployment recording for that tag.
Expand Down Expand Up @@ -155,7 +163,7 @@ Use `miftah --help` for the generated command list and `miftah <command> --help`

| Command | Purpose |
| --- | --- |
| `miftah --config <file>` / `miftah serve --config <file>` | Run the STDIO MCP wrapper. |
| `miftah --config <file>` / `miftah serve --config <file>` | Run the STDIO MCP wrapper. Add `--transport http` to serve the configured local Streamable HTTP endpoint. |
| `miftah validate --config <file>` | Parse and validate JSON config; writes JSON. |
| `miftah doctor --config <file> [--json]` | Report redacted configuration and upstream readiness. |
| `miftah init [name] [--name <name>] [--preset <name>] [--output <file>] [--interactive] [--client <client>] [--credential-env <name>] [--npm-package <package>] [--docker-image <image>] [--url <url>] [--header-name <name>] [--header-prefix <prefix>]` | Generate a strict catalog template and optionally print client JSON. |
Expand Down Expand Up @@ -186,7 +194,7 @@ Structured success output is written to stdout with stderr empty. Stable nonzero

## Current boundaries

The current experimental code implements local STDIO and remote HTTP/SSE upstream clients, profile switching with opt-in scoped persistence, hybrid routing rules plus provider routing matchers, policies, optional upstream identity verification, namespaced tools/resources/prompts for account bundles, resilient healthy-upstream discovery, configurable local process lifecycle controls, in-memory process/session caching, rotated redacted JSONL audit journals with optional local tamper evidence, and a packageable CLI. Local process controls cover startup and shutdown deadlines, optional idle cleanup, opt-in crash recovery with a bounded retry budget, and no-eviction profile-session capacity limits. UI, routing plugins, `process.startMode`, `process.cache`, custom state paths, and configurable tool namespaces are rejected with `UNSUPPORTED_CONFIG_OPTION` rather than silently ignored.
The current experimental code implements local STDIO plus loopback-first Streamable HTTP serving, remote HTTP/SSE upstream clients, profile switching with opt-in scoped persistence, hybrid routing rules plus provider routing matchers, policies, optional upstream identity verification, namespaced tools/resources/prompts for account bundles, resilient healthy-upstream discovery, configurable local process lifecycle controls, in-memory process/session caching, rotated redacted JSONL audit journals with optional local tamper evidence, and a packageable CLI. HTTP sessions are bounded, isolated, and cleaned up on expiry, DELETE, or shutdown; local process controls cover startup and shutdown deadlines, optional idle cleanup, opt-in crash recovery with a bounded retry budget, and no-eviction profile-session capacity limits. UI, routing plugins, `process.startMode`, `process.cache`, custom state paths, and configurable tool namespaces are rejected with `UNSUPPORTED_CONFIG_OPTION` rather than silently ignored.

## License

Expand Down
19 changes: 12 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ Miftah is an MCP-aware proxy, not a byte-level reverse proxy:

```text
MCP client
-> Miftah STDIO server
-> config and secret resolver
-> profile state
-> routing and policy
-> cached upstream MCP client
-> upstream STDIO server
-> Miftah transport
-> STDIO server (one process runtime)
-> HTTP `/mcp` host (one isolated runtime per client session)
-> config and secret resolver
-> profile state
-> routing and policy
-> cached upstream MCP client
-> upstream STDIO server
```

The public server is built with the official `@modelcontextprotocol/sdk` `Server` and `StdioServerTransport`. Each profile/upstream pair gets an SDK `Client` and its configured transport on first use: local processes use `StdioClientTransport`; remote upstreams use Streamable HTTP by default (`"http"` is its compatibility alias) or the deprecated legacy SSE client. Resolved profile headers override upstream headers case-insensitively before either remote transport is constructed. Streamable HTTP session shutdown sends DELETE while its session ID is still available, then closes the local client transport; legacy SSE has no corresponding server-session teardown request. Request cancellation and requested progress notifications retain their MCP request context across both STDIO and Streamable HTTP upstreams. Startup HTTP failures and operation HTTP/MCP protocol failures are normalized before crossing the proxy boundary, retaining only stable category/code metadata rather than server response text.
The STDIO public server is built with the official `@modelcontextprotocol/sdk` `Server` and `StdioServerTransport`. Each profile/upstream pair gets an SDK `Client` and its configured transport on first use: local processes use `StdioClientTransport`; remote upstreams use Streamable HTTP by default (`"http"` is its compatibility alias) or the deprecated legacy SSE client. Resolved profile headers override upstream headers case-insensitively before either remote transport is constructed. Streamable HTTP session shutdown sends DELETE while its session ID is still available, then closes the local client transport; legacy SSE has no corresponding server-session teardown request. Request cancellation and requested progress notifications retain their MCP request context across both STDIO and Streamable HTTP upstreams. Startup HTTP failures and operation HTTP/MCP protocol failures are normalized before crossing the proxy boundary, retaining only stable category/code metadata rather than server response text.

The optional HTTP host owns the listener and no shared Miftah session state. It validates the exact `/mcp` target, Host, Origin, bearer authentication, a bounded JSON initialize request, and capacity before creating a `StreamableHTTPServerTransport`. That transport and a new `MiftahServer`, `ProfileManager`, and upstream manager belong to one HTTP session only. The host forces the session runtime to in-memory `session` profile state, even if normal STDIO configuration uses workspace/global persistence. Its session registry owns only opaque MCP session IDs, idle timers, and cleanup tasks; DELETE, expiry, connection shutdown, and wrapper shutdown detach the record before asynchronous runtime cleanup so no client can reuse a closing runtime, while retaining that capacity reservation until cleanup succeeds. A rejected cleanup is recorded, retains capacity, and makes graceful shutdown fail rather than silently admitting unlimited replacement runtimes. A reconnect is routed to the existing transport by its session ID, while the SDK permits only one active standalone SSE stream for that session.

Per-profile start and restart single-flight state, attempt tokens, and lifecycle generations prevent concurrent calls or stale transport close events from creating duplicate children or replacing a newer session. Operations report begin/end activity so idle shutdown cannot close an in-flight request; a resource subscription retains that activity until it is unsubscribed or invalidated. Unexpected close is health `failed`; idle, explicit restart, wrapper shutdown, and timeout-enforced shutdown are intentional `stopped` states with a reason. Optional crash recovery uses bounded exponential backoff with jitter and a terminal retry budget; it retains its capacity reservation across the backoff window. Streamable HTTP additionally uses the SDK's bounded SSE reconnection behavior before a terminal transport closure reaches manager-level recovery. A shared no-eviction limiter counts distinct profile bundles across named upstreams, so capacity pressure returns a typed refusal instead of terminating a live credential session. Tool discovery is held in immutable, per-profile capability snapshots; concurrent callers share one discovery operation. Complete snapshots are cached, while a permissive partial snapshot remains callable but is retried by the next tool list or call request. A call captures its active profile before discovery, so a later profile switch cannot change that call's active-profile fallback.

Expand All @@ -39,6 +43,7 @@ The server advertises `resources.listChanged` and `prompts.listChanged` with `to
Configuration and runtime concerns are intentionally separate:

- `config/` parses and validates JSON, expands paths, and provides schema output.
- `http/` owns the loopback-first Streamable HTTP listener, ingress validation, bounded per-client session registry, and isolated runtime cleanup.
- `secrets/` owns a typed provider contract for environment, dotenv, opt-in plaintext, OS keychain, and 1Password resolution; it canonicalizes external references, bounds child processes, and registers each resolved value with shared redaction before returning it.
- `isolation/` owns POSIX profile runtime trees, copy-only credential materialization, container bind-mount argument generation, and the isolation stderr boundary.
- `profiles/` owns active profile state, scope-aware persistence, and switching restrictions.
Expand Down
10 changes: 8 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CLI reference

`miftah` is a local STDIO MCP wrapper. Run `miftah --help` for the generated command list, or `miftah <command> --help` for the options accepted by one command. The help text is the authoritative grammar for the installed version.
`miftah` is an MCP wrapper with STDIO as its default transport and an opt-in local Streamable HTTP server. Run `miftah --help` for the generated command list, or `miftah <command> --help` for the options accepted by one command. The help text is the authoritative grammar for the installed version.

## Help

Expand All @@ -16,7 +16,7 @@ The root command list is `serve`, `validate`, `doctor`, `schema`, `init`, `list-

| Command | Required input | Options | Output and behavior |
| --- | --- | --- | --- |
| `miftah serve --config <file>` | `--config` | `--config <file>` | Runs the STDIO MCP wrapper until it is stopped. `miftah --config <file>` is the equivalent default-command form. |
| `miftah serve --config <file>` | `--config` | `--config <file>`, `--transport <stdio\|http>` | Runs the STDIO MCP wrapper by default, or the configured local Streamable HTTP endpoint with `--transport http`. `miftah --config <file>` is the equivalent default-command STDIO form. |
| `miftah validate --config <file>` | `--config` | `--config <file>` | Validates the JSON configuration without starting an upstream. Writes a JSON object with `ok`, `name`, and `profiles`. |
| `miftah doctor --config <file>` | `--config` | `--config <file>`, `--json` | Validates configuration and checks upstream readiness. Default output is a human-readable report; `--json` writes only the JSON report. A healthy or degraded report exits `0`; a failed report exits `1`. |
| `miftah schema` | none | none | Writes the Miftah JSON Schema as pretty-printed JSON. |
Expand All @@ -30,6 +30,12 @@ The root command list is `serve`, `validate`, `doctor`, `schema`, `init`, `list-

Every command also accepts `--help` and `-h`; those generated per-command help screens show only the options valid for that command.

### `serve` transports

`miftah serve --config <file>` and `miftah serve --transport stdio --config <file>` accept one STDIO client transport. `miftah serve --transport http --config <file>` starts the `/mcp` Streamable HTTP endpoint from `server.http`; it defaults to `http://127.0.0.1:3000/mcp` when that configuration is absent. The listener URL is written to stderr so STDIO's MCP protocol stream remains untouched. Signals stop new HTTP admissions and close the per-session runtimes and upstream transports.

HTTP bearer authentication is configured only through `server.http.authToken` as a secret reference. The CLI never accepts a bearer token option and never writes one to its listener or error output. See [HTTP server transport](config.md#http-server-transport) for loopback, non-loopback, Host, Origin, session, and request-limit requirements.

### `init` presets and paths

`--preset` defaults to `generic`. The strict catalog accepts `generic`, `github`, `sentry`, `generic-npx`, `generic-docker`, and `streamable-http`; an unrecognized preset is a usage error. `--output` defaults to `<name>.miftah.json`. Miftah resolves the output path from the current working directory, creates missing parent directories, and refuses to overwrite an existing file. Quote shell paths and names containing spaces:
Expand Down
43 changes: 43 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,49 @@ On macOS Miftah runs the fixed `/usr/bin/security find-generic-password -s <serv

For `secretref:op`, noninteractive launches require an inherited `OP_SERVICE_ACCOUNT_TOKEN`; Miftah fails closed before invoking `op` when it is absent. Interactive 1Password desktop/CLI authentication is allowed only when both standard input and output are TTYs, and remains subject to `providerTimeoutMs`. Miftah registers a successful provider value and the inherited service-account token with the shared redactor before either can cross an error, audit, health, or response boundary.

## HTTP server transport

`miftah serve --transport http --config <file>` exposes Miftah's Streamable HTTP endpoint at `/mcp`. This is a local Miftah server transport, distinct from an `upstream.transport: "streamable-http"` remote upstream. The default listener is the literal loopback endpoint `http://127.0.0.1:3000/mcp`.

Configure the listener under `server.http`:

```json
{
"server": {
"http": {
"host": "127.0.0.1",
"port": 3000,
"maxSessions": 32,
"sessionIdleTimeoutMs": 900000,
"maxRequestBytes": 1048576
}
}
}
```

`port` accepts `0` through `65535` (`0` chooses an OS-assigned port); `maxSessions` is `1` through `256`; `sessionIdleTimeoutMs` is `1,000` through `86,400,000`; and `maxRequestBytes` is `1,024` through `10,485,760`. Miftah validates and bounds a POST body before it allocates a session. A session is removed on MCP DELETE, idle expiry, or shutdown; removal closes that session's runtime and retained upstream transports. A closing session is no longer addressable, but retains its capacity reservation until cleanup succeeds; a cleanup failure is reported and conservatively retains the reservation.

Only literal `127.0.0.1` and `::1` are loopback bind values. A hostname, `localhost`, `0.0.0.0`, or any other address is a deliberate non-loopback exposure and must include all of the following:

```json
{
"server": {
"http": {
"host": "0.0.0.0",
"port": 8443,
"allowNonLoopback": true,
"authToken": "${MIFTAH_HTTP_TOKEN}",
"allowedHosts": ["mcp.example.test"],
"allowedOrigins": ["https://client.example.test"]
}
}
}
```

`authToken` is an exact environment or `secretref:` reference, never a plaintext token. `allowedHosts` is an exact canonical host allowlist (the request port is ignored); it defaults to the bind host only for literal-loopback configuration. An absent `Origin` is permitted for non-browser MCP clients. A present `Origin` is rejected unless it exactly matches `allowedOrigins`; no permissive CORS response is emitted. Do not put a bearer token in CLI arguments, URLs, or configuration literals.

Each accepted HTTP session owns a fresh Miftah runtime, profile manager, routing/approval/lock/lease state, and upstream manager. HTTP sessions always use in-memory `session` profile state, even when the base configuration opts into durable state, so one client's selection cannot persist into or alter another client's session.

## Profile credential isolation

`profiles.<profile>.isolation` is an opt-in, POSIX-only filesystem boundary for a local STDIO target. Miftah derives a deterministic runtime tree from the canonical configuration file, profile, and upstream name; it never accepts an operator-selected runtime root. The tree contains `home`, `appdata`, `localappdata`, and `xdg/config`, `xdg/cache`, `xdg/data`, `xdg/state`, and `xdg/runtime`. On macOS and Linux, Miftah verifies owner control, uses restrictive `0700` directories and `0600` copied files where the platform supports those modes, and refuses a reused target without its matching Miftah marker.
Expand Down
4 changes: 3 additions & 1 deletion docs/library-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ await runtime.connect(new StdioServerTransport());

## Type exports

The configuration contract exposes `ActiveProfileStateScope`, `AuditConfig`, `AuditIntegrityConfig`, `AuditRotationConfig`, `GitHubProfileRoutingMatch`, `IdentityConfig`, `IdentityFingerprint`, `IdentityProbeConfig`, `JiraProfileRoutingMatch`, `LinearProfileRoutingMatch`, `MiftahConfig`, `PolicyConfig`, `PostHogProfileRoutingMatch`, `ProcessConfig`, `ProfileConfig`, `ProfileIsolationConfig`, `ProfileIsolationContainerVolume`, `ProfileIsolationFile`, `ProfileLeaseConfig`, `ProfileRoutingConfig`, `ProfileRoutingMatchConfig`, `ProfileUpstreamOverride`, `RiskLevel`, `RoutingConfig`, `RoutingRule`, `SecurityConfig`, `SentryProfileRoutingMatch`, `SecretsConfig`, `StateConfig`, `ToolDiscoveryMode`, `ToolingConfig`, `TransportType`, `UnknownToolRisk`, `UpstreamConfig`, and `ValidatedRoutingConfig`.
The configuration contract exposes `ActiveProfileStateScope`, `AuditConfig`, `AuditIntegrityConfig`, `AuditRotationConfig`, `GitHubProfileRoutingMatch`, `HttpServerConfig`, `IdentityConfig`, `IdentityFingerprint`, `IdentityProbeConfig`, `JiraProfileRoutingMatch`, `LinearProfileRoutingMatch`, `MiftahConfig`, `PolicyConfig`, `PostHogProfileRoutingMatch`, `ProcessConfig`, `ProfileConfig`, `ProfileIsolationConfig`, `ProfileIsolationContainerVolume`, `ProfileIsolationFile`, `ProfileLeaseConfig`, `ProfileRoutingConfig`, `ProfileRoutingMatchConfig`, `ProfileUpstreamOverride`, `RiskLevel`, `RoutingConfig`, `RoutingRule`, `SecurityConfig`, `SentryProfileRoutingMatch`, `SecretsConfig`, `ServerConfig`, `StateConfig`, `ToolDiscoveryMode`, `ToolingConfig`, `TransportType`, `UnknownToolRisk`, `UpstreamConfig`, and `ValidatedRoutingConfig`.

`AuditRotationConfig` requires `retainFiles` (maximum `2000`) plus at least one positive trigger (`maxBytes` or `maxAgeMs`). `AuditIntegrityConfig` currently exposes the explicit local `"sha256-chain"` option only. Both configure the runtime journal; journal writers, readers, and CLI export/verification implementations remain internal.

`StateConfig` makes active-profile persistence explicit. Its durable `workspace` and `global` scopes require `persistActiveProfile: true`; custom state-file paths are intentionally not part of the public API.

`ServerConfig` and `HttpServerConfig` configure the CLI-owned literal-loopback-first Streamable HTTP host. Authentication values are supported secret references, not command-line token values; non-loopback binds require explicit opt-in, a bearer token, and exact allowed hosts.

`UpstreamConfig.trustToolAnnotations` is opt-in and defaults to false. `ToolingConfig.unknownToolRisk` uses the exported `UnknownToolRisk` union (`"write" | "destructive"`) and defaults to `"destructive"`; callers can use exact `toolRiskOverrides` for known read tools.

`ProfileRoutingConfig` describes opt-in identifiers for Miftah's fixed in-tree provider matchers. It is declarative configuration only: it does not load third-party code, resolve secrets, or grant a matcher access to process or network APIs.
Expand Down
Loading
Loading