Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
45 changes: 45 additions & 0 deletions .github/RELEASE_NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,48 @@ A tool's approval record, search-index entry and callability are now keyed by th
- **Unresolved names are refused for everyone.** A call to a tool that a connected server's discovered tool set does not contain is refused before any upstream call, for administrators too — while the server's discovery has not completed, retry shortly; afterwards, refresh with `retrieve_tools` and retry with a listed name. Quarantined, disabled and disconnected servers keep their existing answers: a call to a disconnected server still gets the not-connected / `reconnect_on_use` answer, and once the server reconnects and completes discovery, a name that result does not list is refused as unresolved — it is never dispatched.

Details: [Security Quarantine → Namespaced tool names](https://docs.mcpproxy.app/features/security-quarantine#namespaced-tool-names) and [Agent Tokens → Target tool tier](https://docs.mcpproxy.app/features/agent-tokens#target-tool-tier).

## Server edition: configuration keys and modes that never did anything are gone

This release removes the server-edition knobs and `auth_broker` modes that were accepted by the validator but had no reader in production. An old `mcp_config.json` still loads; what changes is how the removed keys are treated. Personal-edition users are not affected unless the file carries a `server_edition` or `auth_broker` block.

**Removed keys and modes** (spec 107, FR-032):

- `server_edition.max_user_servers` and `server_edition.workspace_idle_timeout` — never enforced.
- Per-server `auth_broker.header` and `auth_broker.header_format` — no request was ever rewritten with them.
- `auth_broker.mode: token_exchange` and `auth_broker.mode: entra_obo` — never implemented. `oauth_connect` is now the only accepted mode.

**What the server edition (`mcpproxy-server`) does with an old file**

- Loading succeeds. Each removed key is dropped with one warning naming it — `server_edition.max_user_servers is no longer supported and was ignored`, `auth_broker.header is no longer supported and was ignored`, and so on. A server whose `auth_broker.mode` is `token_exchange` or `entra_obo` loses its **whole** `auth_broker` block: `auth_broker.mode "token_exchange" was never implemented; the auth_broker block for server "<name>" was ignored`. The next write-back of the file omits the dropped keys.
- Writing them is refused. `PATCH /api/v1/config` and `/api/v1/config/apply` reject a document that carries any removed key or mode with the same message, so a script that still sends `max_user_servers` now gets a validation error instead of a silent accept.
- Remove the keys from your file, and change any `token_exchange`/`entra_obo` server to `oauth_connect` if you want its connect flow to keep working — otherwise its `auth_broker` block is dropped on the next load.

**What the personal edition (`mcpproxy`) does with the same file**

- Nothing. The `server_edition` block and every server's `auth_broker` block now pass through the personal binary as opaque JSON — every key and value preserved, removed keys included, no warning and no validation. Earlier releases wrote both blocks back as `{}`, so an API-key bootstrap or a `PATCH /api/v1/config` from the personal binary could erase a team's SSO or broker configuration; that is fixed here (FR-040).

## `store_idp_tokens` is now a no-op

`server_edition.store_idp_tokens` no longer stores anything. The identity-provider access and refresh tokens it used to persist at login existed only to feed the never-implemented `token_exchange`/`entra_obo` modes, which left a long-lived IdP refresh token at rest with nothing reading it. The writer, the reader and the offline-access scope and authorization parameters that asked the IdP for a refresh token (`offline_access`, `access_type=offline`) are removed (FR-033), so a fresh login no longer requests a refresh token from the IdP.

- The key is still accepted so an old file loads. `"store_idp_tokens": true` logs one warning at boot — `server_edition.store_idp_tokens is deprecated and no longer stores IdP tokens; remove it` — and does nothing else.
- Remove it from your configuration. Nothing in this release reads the IdP tokens an earlier release stored, and the first start of `mcpproxy-server` with `server_edition.enabled: true` after upgrading **deletes them** from `config.db` (the rows are removed by key before anything else in the server-edition setup runs, so this happens whether `MCPPROXY_CRED_KEY` is still set, unset or even invalid; the log line `purged legacy IdP subject-token rows` reports the count). Credentials connected through the `oauth_connect` flow are not touched.
- The former [IdP Token Storage](https://docs.mcpproxy.app/features/idp-token-storage/) page is now a tombstone.

## Auth broker: a stored credential is stored, not injected

The `oauth_connect` connect flow, its REST routes, the `mcpproxy credential` commands, the encrypted credential store and `MCPPROXY_CRED_KEY` / `credential_encryption_key` all stay. What changes is the promise attached to them: a credential a user connects through the broker is **kept for a future broker and is not injected into upstream tool calls**. It never was — the injection, resolution and per-user connection-keying code paths that the documentation described had no production caller and are deleted in this release (FR-031, FR-034).

- `mcpproxy credential list` and `mcpproxy credential status` now open with the line `Stored credentials are kept for a future broker and are NOT injected into upstream calls in this release.`
- The [auth broker](https://docs.mcpproxy.app/features/auth-broker/) and [credential commands](https://docs.mcpproxy.app/cli/credential-commands/) pages are rewritten accordingly; the "Credential resolution", "Header injection" and "Per-(user, server) connection keying" sections are gone.
- Upstream calls keep using whatever the server's own configuration provides (static headers, the server's own OAuth). If you deployed the broker expecting per-user credentials on upstream calls, that expectation was never met, and this release says so rather than fixing it.
- Historical `credential_broker` activity rows remain readable and labelled.

## Agent tokens: a per-user quota inside the deployment cap

The server edition now enforces a **25-token quota per signed-in user** on top of the existing 100-record deployment cap ([#1177](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/1177)). Revoked tokens keep their slot until they are permanently deleted.

- Server edition: a user at 25 tokens gets a `409 Conflict` from `POST /user/tokens` that names *their* quota — permanently deleting one of their unused tokens frees a slot — so one user can no longer take the whole pool. The 100-record deployment cap remains and every stored token still counts toward it, so a deployment whose stored records add up to 100 refuses the next token for everyone until an administrator frees records; a caller who is still under their own quota then gets the `409` that says the limit is shared and points at an administrator (the quota is checked first, so a user already at 25 sees their own-quota message instead). A user who already holds more than 25 tokens keeps them; they cannot create another until they are back under the quota.
- Personal edition: every token is ownerless, so the quota does not apply and the 100-token limit is unchanged.
- No configuration change is needed. Details: [agent tokens](https://docs.mcpproxy.app/features/agent-tokens/).
31 changes: 30 additions & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,18 @@ jobs:
# cache-invalidation fix.
skip-cache: true

# Second pass under the server build tag (Spec 107 FR-047). Everything in
# internal/serveredition/ and every *_serveredition.go file is behind
# `//go:build server`, so the bare run above never sees it; without this
# step the server edition ships unlinted. Same config, same version — the
# only difference is the tag set.
- name: golangci-lint (server edition)
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.9.0
args: --config .github/.golangci.yml --build-tags server --timeout=10m
skip-cache: true

server-edition:
name: Server Edition
runs-on: ubuntu-latest
Expand All @@ -328,6 +340,14 @@ jobs:
- name: Build server edition
run: go build -tags server ./cmd/mcpproxy

# internal/server is now in the list below and constructs the tiktoken
# tokenizer; warm the vocabulary once so parallel packages hit the cache
# (same reason as the unit-tests job above).
- name: Warm the tiktoken vocabulary cache
run: go run ./bench/cmd/warmtiktoken
env:
TIKTOKEN_CACHE_DIR: ${{ runner.temp }}/tiktoken

# internal/oauth is where the redaction rules live, and half of them are
# build-tagged: serverfields_serveredition.go holds the mask decisions for
# the `auth_broker` block, whose fields do not exist in the personal
Expand All @@ -336,8 +356,17 @@ jobs:
# `-tags server`. Without this package in the list they never ran, and the
# guarantee the #1161 fix rests on (a field added to the tagged block is
# masked because the walk reaches it) was unenforced in CI.
#
# internal/server, internal/httpapi and internal/storage carry the
# server-edition wiring (serveredition_wire.go, the tagged handlers, the
# agent-token store) and were only ever *built* under the tag, never
# race-tested (Spec 107 FR-047). The -skip regex is the one the unit-test
# job uses (see "Run unit tests" above): it drops the binary-spawning
# internal/server tests that otherwise hang to the package timeout.
- name: Test server edition packages
run: go test -race -tags server ./internal/serveredition/... ./internal/config/... ./internal/oauth/...
run: go test -race -tags server -timeout 20m -skip "E2E|Binary|MCPProtocol|TestInfoEndpoint|TestGracefulShutdownNoPanic|TestSocketInfoEndpoint" ./internal/serveredition/... ./internal/config/... ./internal/oauth/... ./internal/server/... ./internal/httpapi/... ./internal/storage/...
env:
TIKTOKEN_CACHE_DIR: ${{ runner.temp }}/tiktoken

build:
name: Build
Expand Down
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Built in two editions from one codebase via Go build tags:
| Edition | Build | Binary | Distribution |
|---------|-------|--------|--------------|
| **Personal** (default) | `go build ./cmd/mcpproxy` | `mcpproxy` | macOS DMG, Windows installer, Linux tar.gz |
| **Server** | `go build -tags server ./cmd/mcpproxy` | `mcpproxy-server` | Docker image, .deb, Linux tar.gz |
| **Server** | `go build -tags server -o mcpproxy-server ./cmd/mcpproxy` | `mcpproxy-server` | Docker image (`ghcr.io`) only — no .deb / tar.gz |

All server code is behind `//go:build server` in `internal/serveredition/`; the personal edition is unaffected. The binary self-identifies (`mcpproxy version`, `/api/v1/status` → `"edition"`). Server multi-user OAuth (Spec 024): see [docs/development/server-edition-multiuser-auth.md](docs/development/server-edition-multiuser-auth.md).

Expand Down Expand Up @@ -78,8 +78,14 @@ go test -tags server ./internal/serveredition/... -race # server edition

# Lint — CI uses golangci-lint v2 with .github/.golangci.yml, which is STRICTER
# than the local scripts/run-linter.sh (v1.x) and catches things it misses.
# Run the v2 binary before pushing:
# CI runs it TWICE: bare, and with --build-tags server (server-edition code is
# invisible to the bare run). Run both before pushing:
/opt/homebrew/bin/golangci-lint run --config .github/.golangci.yml ./...
/opt/homebrew/bin/golangci-lint run --config .github/.golangci.yml --build-tags server ./...
# CI also race-tests internal/server, httpapi and storage under -tags server
# with the unit-tests.yml -skip regex (bare `go test ./internal/server/...`
# hangs to the timeout on the binary-spawning tests):
go test -race -tags server -timeout 20m -skip "E2E|Binary|MCPProtocol|TestInfoEndpoint|TestGracefulShutdownNoPanic|TestSocketInfoEndpoint" ./internal/serveredition/... ./internal/config/... ./internal/oauth/... ./internal/server/... ./internal/httpapi/... ./internal/storage/...

# Run
./mcpproxy serve [--listen :8080] [--log-level=debug] # core (localhost:8080)
Expand Down
45 changes: 36 additions & 9 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,13 @@ graph LR
- 🔵 **Release qualification gate (auto-QA matrix blocks the tag)** — In progress · P0
- 🔵 **MCP protocol upgrade to 2026-07-28 revision** — In progress · P1
- 🔵 **Planning/docs truth automation** — In progress · P2
- 🔵 **Spec 107 server edition SSO front door hardened for real IdPs** — In progress · P2
- 🔵 **Discovery-quality eval harness (Spec 065 second half)** — In progress · P3
- ⚪ **Windows native tray app** — Todo · P2
- ⚫ **Server marketplace** — Todo · P3 · parked
- ⚫ **Audit SIEM integration** — Todo · P3 · parked
- ⚫ **Paid-tier MVP (billing / seats / license)** — Todo · P3 · parked
- ⚫ **SDK v1 migration** — Todo · P3 · parked
- ⚫ **SSO (server edition)** — Todo · P3 · parked
- ⚪ **Security gateway Tracks C/D (per-arg least-privilege + signature provenance)** — Todo · P3
- 🟢 **Upgrade awareness & guided update** — Done · P0
- 🟢 **Connect step trust: preview, visible backup, one-click undo** — Done · P0
Expand Down Expand Up @@ -405,6 +405,39 @@ graph LR

</details>

<details>
<summary>🔵 Spec 107 server edition SSO front door hardened for real IdPs — In progress · P2</summary>

> Generic OIDC, IdP-group -> server allowlist, attributable JSONL audit line; freeze the latent multiuser/credential-injection code. Research: docs/research/server-edition-2026-09-14 (#1281).

Spec: [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/)

```mermaid
graph LR
sso_pr_a_freeze_cut["PR-A freeze/cut latent code + config normalis…"]
sso_pr_b_oidc_front_door["PR-B generic OIDC provider + front door behin…"]
sso_pr_c_group_allowlist["PR-C one entitlement predicate, group grants,…"]
sso_pr_d_audit_line["PR-D attributable JSONL audit line + auth_eve…"]

sso_pr_a_freeze_cut --> sso_pr_b_oidc_front_door
sso_pr_b_oidc_front_door --> sso_pr_c_group_allowlist
sso_pr_c_group_allowlist --> sso_pr_d_audit_line

classDef in_progress fill:#1f6feb,stroke:#0b3d91,color:#ffffff;
classDef todo fill:#6e7781,stroke:#3d4248,color:#ffffff;
class sso_pr_a_freeze_cut in_progress;
class sso_pr_b_oidc_front_door,sso_pr_c_group_allowlist,sso_pr_d_audit_line todo;
```

| Task | Status | Refs |
| --- | --- | --- |
| PR-A freeze/cut latent code + config normaliser + per-owner token cap (US5, US6) | 🔵 In progress | #1287 |
| PR-B generic OIDC provider + front door behind ingress + telemetry v13 (US2, US7) | ⚪ Todo | — |
| PR-C one entitlement predicate, group grants, tenant Web UI session principal (US1, US4) | ⚪ Todo | — |
| PR-D attributable JSONL audit line + auth_event + config/doctor/metrics (US3) | ⚪ Todo | — |

</details>

<details>
<summary>🔵 Discovery-quality eval harness (Spec 065 second half) — In progress · P3</summary>

Expand Down Expand Up @@ -600,13 +633,6 @@ Spec: [054-mcp-security-gateway](./specs/054-mcp-security-gateway/)

</details>

<details>
<summary>⚫ SSO (server edition) — Todo · parked · P3</summary>

> PARKED. Single sign-on for the multi-user server edition.

</details>

<details>
<summary>🟢 Upgrade awareness &amp; guided update — Done · P0</summary>

Expand Down Expand Up @@ -868,6 +894,7 @@ graph LR
| Telemetry v7: honest funnel + churn instrumentation | In progress | P1 | — | [080-telemetry-v7-churn](./specs/080-telemetry-v7-churn/) | |
| MCP protocol upgrade to 2026-07-28 revision | In progress | P1 | 19/81 (23%) | [058-mcp-2026-upgrade](./specs/058-mcp-2026-upgrade/) | |
| Planning/docs truth automation | In progress | P2 | — | | |
| Spec 107 server edition SSO front door hardened for real IdPs | In progress | P2 | 32/126 (25%) | [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | |
| Discovery-quality eval harness (Spec 065 second half) | In progress | P3 | — | [065-evaluation-foundation](./specs/065-evaluation-foundation/) | |
| tpa-db: versioned TPA signature database for the offline scanner | Todo | P1 | — | [101-tpa-db](./specs/101-tpa-db/) | |
| Auto routing mode: budget-fitted tool surface per session (spec 104) | Todo | P1 | — | [104-auto-routing-mode](./specs/104-auto-routing-mode/) | |
Expand All @@ -879,7 +906,6 @@ graph LR
| Audit SIEM integration `MCP-39` | Todo (parked) | P3 | — | | |
| Paid-tier MVP (billing / seats / license) `MCP-40` | Todo (parked) | P3 | — | | |
| SDK v1 migration | Todo (parked) | P3 | — | | |
| SSO (server edition) | Todo (parked) | P3 | — | | |
| Upgrade awareness & guided update | Done | P0 | — | [079-upgrade-nudge](./specs/079-upgrade-nudge/) | |
| Connect step trust: preview, visible backup, one-click undo | Done | P0 | — | [078-connect-trust-preview](./specs/078-connect-trust-preview/) | |
| Non-Docker sandbox isolation (Landlock) `MCP-34` | Done | P1 | — | | |
Expand Down Expand Up @@ -1010,3 +1036,4 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—`
| [104-auto-routing-mode](./specs/104-auto-routing-mode/) | — | — |
| [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | `drafted` | 0/109 (0%) |
| [106-security-residual-fixes](./specs/106-security-residual-fixes/) | `shipped` | 18/19 (95%) |
| [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | `in-flight` | 32/126 (25%) |
14 changes: 14 additions & 0 deletions cmd/mcpproxy/cli_config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package main

import (
"io"
"os"

"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
)

// cliDiagnosticsWriter receives the loader's removed-key / deprecated-key
// findings (Spec 107 FR-032) for every CLI command that loads the config
// through loadCLIConfig. It is stderr so structured stdout (`-o json`) stays
// clean; tests swap it for a buffer.
var cliDiagnosticsWriter io.Writer = os.Stderr

// loadCLIConfig loads a CLI command's config from explicitPath (the command's
// --config flag) when set, falling back to the default search path, and applies
// the global --data-dir flag on top (GH #854/#897/#908). Without the DataDir
Expand All @@ -22,6 +31,11 @@ func loadCLIConfig(explicitPath string) (*config.Config, error) {
if err != nil {
return nil, err
}
// The server-build loader may have dropped removed keys / retired
// auth_broker modes and recorded a diagnostic each; several callers
// SaveConfig the result, so the operator must see the drop here (the
// personal build records none — opaque carriers, FR-040).
config.WriteLoadDiagnostics(cfg, cliDiagnosticsWriter)
if dataDir != "" {
cfg.DataDir = dataDir
}
Expand Down
30 changes: 30 additions & 0 deletions cmd/mcpproxy/cli_config_diagnostics_personal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//go:build !server

package main

import (
"bytes"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestLoadCLIConfig_PersonalBuildEmitsNoDiagnostics: the personal build
// carries server_edition and auth_broker as opaque blocks (FR-040) and
// records no diagnostics, so the CLI door prints nothing for the legacy
// fixture.
func TestLoadCLIConfig_PersonalBuildEmitsNoDiagnostics(t *testing.T) {
var stderr bytes.Buffer
prev := cliDiagnosticsWriter
cliDiagnosticsWriter = &stderr
t.Cleanup(func() { cliDiagnosticsWriter = prev })

path, err := filepath.Abs(filepath.Join("..", "..", "internal", "config", "testdata", "legacy_server_edition.json"))
require.NoError(t, err)
cfg, err := loadCLIConfig(path)
require.NoError(t, err)
assert.Empty(t, cfg.LoadDiagnostics())
assert.Empty(t, stderr.String())
}
Loading
Loading