diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml new file mode 100644 index 0000000..125fae6 --- /dev/null +++ b/.github/workflows/auto-tag.yml @@ -0,0 +1,62 @@ +name: Auto Tag Release + +# Cuts the release tag automatically when a version bump lands on main. +# The tag then flows through the Release workflow (CI gate, npm publish via +# OIDC trusted publishing, GitHub Release, MCP Registry). + +on: + push: + branches: + - main + paths: + - package.json + +permissions: + contents: write + actions: write + +jobs: + tag: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Tag new version and dispatch Release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + version="$(node -p "require('./package.json').version")" + tag="v${version}" + + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Refusing to tag non-semver version: $version" >&2 + exit 1 + fi + + git fetch --tags --force + if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + echo "Tag $tag already exists; nothing to do." + exit 0 + fi + + server_version="$(node -p "require('./server.json').version")" + server_pkg_version="$(node -p "require('./server.json').packages[0].version")" + if [[ "$server_version" != "$version" || "$server_pkg_version" != "$version" ]]; then + echo "server.json ($server_version / package $server_pkg_version) does not match package.json ($version); not tagging." >&2 + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "$tag" -m "Release $tag" + git push origin "$tag" + + # Tags pushed with GITHUB_TOKEN do not fire the tag-push trigger on + # other workflows, so dispatch the Release workflow explicitly. + gh workflow run release.yml -f tag="$tag" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c87b5f..a871153 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: - name: Lint run: npm run lint + - name: Knip (unused exports/deps) + run: npm run knip + - name: Typecheck run: npm run typecheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ae25f3..776cba5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,9 +69,81 @@ jobs: gh release create "$tag" "${args[@]}" - publish-registry: + publish-npm: needs: ci runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + # OIDC token for npm trusted publishing; npm also derives provenance + # attestation from it, so no long-lived npm token exists anywhere. + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + cache: npm + + - name: Resolve tag + id: tag + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + + tag="${GITHUB_REF_NAME}" + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + tag="$INPUT_TAG" + fi + + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Refusing to publish non-semver tag: $tag" >&2 + exit 1 + fi + + echo "version=${tag#v}" >>"$GITHUB_OUTPUT" + + - name: Verify package.json matches tag + env: + VERSION: ${{ steps.tag.outputs.version }} + run: | + set -euo pipefail + + package_version="$(node -p "require('./package.json').version")" + if [[ "$package_version" != "$VERSION" ]]; then + echo "package.json ($package_version) does not match tag v$VERSION" >&2 + exit 1 + fi + + - name: Install dependencies + run: npm ci + + - name: Publish to npm + env: + VERSION: ${{ steps.tag.outputs.version }} + run: | + set -euo pipefail + + if npm view "portkey-admin-mcp@${VERSION}" version >/dev/null 2>&1; then + echo "portkey-admin-mcp@${VERSION} is already on npm; nothing to do." + exit 0 + fi + + # Authenticates via OIDC trusted publishing (configured on npmjs.com + # for this repo + workflow). prepublishOnly re-runs the full CI suite. + npm publish --access public + + publish-registry: + needs: [ci, publish-npm] + runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a22660..d723788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.7] - 2026-06-11 + +Security hardening, pagination params, compact tool responses, and a major test-coverage expansion from a four-domain code review. Tool-param additions are additive; no breaking API surface changes. + +### Security + +- Sanitize the caller-supplied `MCP-Protocol-Version` header before echoing it in HTTP error responses — truncated to 64 chars and restricted to `[A-Za-z0-9._-]`, closing an unvalidated-input reflection path. +- Remove Redis configuration details from the unauthenticated `/auth/info` response to reduce infrastructure fingerprinting. +- Send `Strict-Transport-Security` only when TLS is enabled, instead of emitting HSTS on plain-HTTP responses. +- Emit a startup warning when `ALLOWED_ORIGINS=*` is combined with `MCP_AUTH_MODE=none` — wildcard CORS with no auth gate is a dangerous misconfiguration, now surfaced at boot rather than silently permitted. +- Hash service-cache map keys with SHA-256 so plaintext API keys are never used as in-process cache identifiers. +- Route health checks through `BaseService` so they receive the same SSRF URL validation and structured error parsing as every other upstream call (previously a bespoke fetch path bypassed both). +- `create_api_key` description now warns that the key secret is returned exactly once and will appear in MCP transcripts and LLM context — store it securely immediately. + +### Added + +- **Pagination params on six list tools** — `list_virtual_keys`, `list_configs`, `list_all_users`, `list_user_invites`, `list_mcp_server_capabilities`, and `list_mcp_server_user_access` now accept optional `current_page`/`page_size` inputs, forwarded to the Portkey Admin API; the two MCP-server lists also surface `has_more` so truncated results are no longer indistinguishable from complete ones. +- **Cross-field validation for `create_api_key`** — the workspace key type now requires `workspace_id` at the Zod schema layer instead of failing inside the handler. +- **140 new tests** across 5 new test files: unit coverage for 13 previously untested tool modules, Clerk JWT auth mode, `DELETE /mcp` and SSE `GET /mcp` session endpoints, abort/timeout and upstream-error propagation paths, query-string and pagination edge cases, and contract schemas with live-recorded fixtures for workspaces and users. Total suite: 269 tests (253 unit/integration + 16 e2e). + +### Changed + +- **Compact JSON tool responses** (~157 call sites) — tool responses no longer pretty-print with 2-space indent, reducing response token usage on every tool call. +- **Lazy Redis import** — the `redis` client module now loads only when the Redis event store is actually constructed, trimming cold-start weight when the event store is `off` or `memory`. +- **`create_integration`/`update_integration` preserve empty strings** — explicitly provided empty-string values (e.g. `custom_host`) are now sent to the API instead of being silently dropped by truthiness checks. +- **`migrate_prompt`/`promote_prompt`** internal prompt lookups now request a small page instead of a full listing. +- **`PORTKEY_BASE_URL` validated once** per service container instead of once per domain service, so misconfiguration fails fast with a single clear error. +- **HTTP transport repositioned as proof of concept** — README and the Vercel guide now state there is no hosted version and stdio is the supported transport. + ## [0.3.6] - 2026-06-05 Corrects the MCP Registry namespace case. No tool schema or API surface changes. @@ -233,7 +262,8 @@ First stable release. Graduates from beta with 151 tools covering ~98% of the Po - Vercel deployment support - Contract tests, E2E tests, security tests -[Unreleased]: https://github.com/CodesWhat/portkey-admin-mcp/compare/v0.3.6...HEAD +[Unreleased]: https://github.com/CodesWhat/portkey-admin-mcp/compare/v0.3.7...HEAD +[0.3.7]: https://github.com/CodesWhat/portkey-admin-mcp/compare/v0.3.6...v0.3.7 [0.3.6]: https://github.com/CodesWhat/portkey-admin-mcp/compare/v0.3.5...v0.3.6 [0.3.5]: https://github.com/CodesWhat/portkey-admin-mcp/compare/v0.3.4...v0.3.5 [0.3.4]: https://github.com/CodesWhat/portkey-admin-mcp/compare/v0.3.3...v0.3.4 diff --git a/ENDPOINTS.md b/ENDPOINTS.md index 9d7539f..c31a748 100644 --- a/ENDPOINTS.md +++ b/ENDPOINTS.md @@ -32,6 +32,8 @@ This document lists all API endpoints used by the Portkey Admin MCP Server, veri **Tested**: Both paths return 403 (permission denied). This is an API key scope issue, not a path issue. Unable to verify correct path. +**Pagination**: `list_all_users` accepts `current_page` (page number, default 1) and `page_size` (results per page, max 100). + --- ## 2. User Invites @@ -49,6 +51,8 @@ This document lists all API endpoints used by the Portkey Admin MCP Server, veri **Tested**: Both paths return 403 (permission denied). This is an API key scope issue, not a path issue. Unable to verify correct path. +**Pagination**: `list_user_invites` accepts `current_page` (page number, default 1) and `page_size` (results per page, max 100). + --- ## 3. User Analytics @@ -110,6 +114,8 @@ This document lists all API endpoints used by the Portkey Admin MCP Server, veri | [x] | DELETE | `/configs/{slug}` | `/configs/{id}` | Delete config | | [x] | GET | `/configs/{slug}/versions` | `/configs/{id}/versions` | List versions | +**Pagination**: `list_configs` accepts `current_page` (page number, default 1) and `page_size` (results per page, max 100). + --- ## 7. Virtual Keys @@ -125,6 +131,8 @@ This document lists all API endpoints used by the Portkey Admin MCP Server, veri | [x] | PUT | `/virtual-keys/{slug}` | `/virtual-keys/{id}` | Update virtual key | | [x] | DELETE | `/virtual-keys/{slug}` | `/virtual-keys/{id}` | Delete virtual key | +**Pagination**: `list_virtual_keys` accepts `current_page` (page number, default 1) and `page_size` (results per page, max 100). + --- ## 8. API Keys @@ -564,4 +572,6 @@ All verified with live API 2026-03-23. List returns `{ object: "list", total, ha | [x] | GET | `/mcp-servers/{id}/user-access` | List user access | | [x] | PUT | `/mcp-servers/{id}/user-access` | Update user access | -All verified with live API 2026-03-23. List returns `{ object: "list", total, data }`. User access returns `{ object: "list", default_user_access, total, has_more, data }`. +All verified with live API 2026-03-23. List returns `{ object: "list", total, data }`. Capabilities list returns `{ total, has_more, capabilities }`. User access returns `{ object: "list", default_user_access, total, has_more, data }`. + +**Pagination**: `list_mcp_server_capabilities` and `list_mcp_server_user_access` accept `current_page` (page number, default 1) and `page_size` (results per page, max 100). diff --git a/README.md b/README.md index f555351..5ebd931 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ If a tool returns a `403` with Portkey error `AB03`, it means missing scopes — ## HTTP Server (Experimental) -> **Status**: The HTTP transport works but hosted deployment is not fully validated for production. Use stdio (npx) for reliable operation. +> **Status**: The HTTP transport works locally and is covered by the integration test suite, but it is a proof of concept — there is **no hosted version** of this server, and hosted deployment is not currently a goal. Use stdio (npx) as the supported transport. The server supports Streamable HTTP for remote access: @@ -202,7 +202,7 @@ For local-only HTTP use, leave `MCP_HOST` at its default `127.0.0.1`. Set `MCP_H
Vercel deployment -Experimental Vercel support is included. See [docs/VERCEL_DEPLOYMENT.md](./docs/VERCEL_DEPLOYMENT.md) for setup instructions. +Vercel support is kept as a reference proof of concept — we do not run a hosted deployment. See [docs/VERCEL_DEPLOYMENT.md](./docs/VERCEL_DEPLOYMENT.md) if you want to self-deploy. Key points: - Uses stateless mode with Redis event store diff --git a/SECURITY.md b/SECURITY.md index 5255936..3d7c2da 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -40,3 +40,13 @@ Rules: See deployment hardening guidance: - [`docs/VERCEL_DEPLOYMENT.md`](./docs/VERCEL_DEPLOYMENT.md) + +## Implementation notes (updated 2026-06-11) + +The following describes the current security posture of the HTTP server (`src/lib/http-app.ts`) and service layer. + +**HSTS.** The `Strict-Transport-Security` header is only emitted when `config.tls.enabled` is true — i.e., when the app is serving native HTTPS. When TLS is handled externally (reverse proxy, Vercel, etc.) the header is suppressed to avoid downgrade issues in mixed-mode deployments. There is no HSTS header in plain-HTTP mode. + +**`/auth/info` endpoint.** This endpoint is intentionally unauthenticated to support client bootstrap (a connecting MCP client must discover auth mode and endpoints before it can obtain a token). The response is limited to: `mode`, `sessionMode`, `eventStoreMode`, `mcpEndpoint`, Clerk config boolean flags (`issuerConfigured`, `jwksConfigured`, `audienceConfigured`), and TLS state. Redis connection details, internal config, and key material are not included. + +**Service cache keys.** API keys are stored as `sha256(apiKey)` Map keys in the in-process service cache. Plaintext key material is not retained in the Map after the initial lookup resolves the cache entry. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 6bc6864..7a32f94 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -3,37 +3,49 @@ This project uses stable SemVer tags plus GitHub Releases so package registries and catalog scanners can detect published versions. -## Publish a New Stable Release +## Publish a New Stable Release (automated) -1. Update `package.json`, `package-lock.json`, `server.json`, and - `CHANGELOG.md` for the new version. Keep `server.json`'s top-level +1. On a dev branch, update `package.json`, `package-lock.json`, `server.json`, + and `CHANGELOG.md` for the new version. Keep `server.json`'s top-level `version` and `packages[0].version` in sync with `package.json`. 2. Run `npm run ci`. -3. Commit the release changes. -4. Publish the npm package: - - ```bash - npm publish --access public - ``` - -5. Create and push a stable tag: - - ```bash - git tag v0.4.0 - git push origin v0.4.0 - ``` - -The `Release` workflow runs two jobs on every pushed `v*` tag: - -- **`github-release`** publishes a non-prerelease GitHub Release for stable - tags like `v0.4.0`. Tags containing a hyphen, such as `v0.4.0-beta.1`, are - published as prereleases and are not marked as the latest release. -- **`publish-registry`** publishes `server.json` to the - [MCP Registry](https://registry.modelcontextprotocol.io). It authenticates - via GitHub Actions OIDC (no secrets required), verifies `server.json` - matches the tag, and waits for the matching npm version to be available - before publishing. Publishing npm before pushing the tag (step 4 then 5) - avoids the wait. +3. Open a PR and merge it to `main`. + +Everything after the merge is automatic: + +- **`Auto Tag Release`** (`auto-tag.yml`) fires when `package.json` changes on + `main`. If the version has no existing tag and `server.json` agrees, it + creates and pushes `vX.Y.Z` and dispatches the `Release` workflow. +- **`Release`** (`release.yml`) re-runs the full CI suite against the tagged + commit, then runs three publish jobs: + - **`publish-npm`** publishes to npm via OIDC trusted publishing with + provenance attestation — no npm token is stored in the repo or CI. It + verifies `package.json` matches the tag and is idempotent (skips if the + version is already on npm). `prepublishOnly` re-runs `npm run ci` as a + final gate. + - **`github-release`** publishes a non-prerelease GitHub Release for stable + tags like `v0.4.0`. Tags containing a hyphen, such as `v0.4.0-beta.1`, are + published as prereleases and are not marked as the latest release. + - **`publish-registry`** publishes `server.json` to the + [MCP Registry](https://registry.modelcontextprotocol.io). It authenticates + via GitHub Actions OIDC, verifies `server.json` matches the tag, and waits + for the matching npm version (already satisfied since it runs after + `publish-npm`). + +### One-time setup: npm Trusted Publisher + +`publish-npm` requires a Trusted Publisher configured on npmjs.com for the +`portkey-admin-mcp` package: Package Settings → Trusted Publisher → GitHub +Actions, with organization `CodesWhat`, repository `portkey-admin-mcp`, and +workflow filename `release.yml` (no environment). Without it the npm publish +step fails with an auth error and the manual fallback below applies. + +## Manual Fallback + +If the automation is unavailable, the old flow still works: `npm publish +--access public` locally from the release commit, then `git tag vX.Y.Z && +git push origin vX.Y.Z` — the `Release` workflow picks the tag up from there +(`publish-npm` skips because the version already exists on npm). ## Backfill an Existing Tag diff --git a/docs/VERCEL_DEPLOYMENT.md b/docs/VERCEL_DEPLOYMENT.md index 090a7db..83c2091 100644 --- a/docs/VERCEL_DEPLOYMENT.md +++ b/docs/VERCEL_DEPLOYMENT.md @@ -1,6 +1,8 @@ # Vercel Deployment Guide (Public Repo Safe) -This guide is the canonical setup for hosting this MCP server on Vercel in 2026. +> **Status (June 2026)**: This deployment path is a **proof of concept kept for reference**. There is no hosted version of this server and hosting is not currently a goal — the supported transport is stdio via `npx`. The steps below remain valid if you want to self-deploy. + +This guide describes how to self-deploy this MCP server on Vercel. ## Recommended Architecture diff --git a/docs/audit-2026-06.md b/docs/audit-2026-06.md index ece1071..f6dbebe 100644 --- a/docs/audit-2026-06.md +++ b/docs/audit-2026-06.md @@ -165,3 +165,52 @@ What the evidence does support is **stopping active feature development** and en 2. Set a calendar reminder for Q3 2026 to check PANW/Prisma AIRS roadmap communications. 3. If no official API changes by Q4 2026, reassess based on actual API drift (re-run endpoint spot-checks, re-record fixtures). 4. If PANW announces a migration target, open a `migrate` track pointing to the self-hosted gateway or whatever replacement API surface they publish. + +--- + +## Follow-up review — 2026-06-11 (dev/0.3.7) + +A second four-agent pass (Security, Code Quality, Performance, Test Coverage) was run against the branch after the initial audit-driven patch cycle. This section records what was fixed and what was deliberately left open. + +### Findings fixed on this branch + +**HTTP / Security** + +- **SEC-2** — `hostValidationMiddleware` now wired in `none`-auth mode only (authenticated modes rely on the token; Host validation would break proxied deployments). Closes the DNS-rebinding gap. +- **SEC-4** — SSRF: `validateUrl` extended with loopback, link-local, and RFC-1918 blocklist in `src/services/base.service.ts`. +- **SEC-5** — `streamId` validated against `^[\w-]{1,128}$` before Redis key construction in `src/lib/event-store.ts`. +- **SEC-7** — Debug logging in `src/services/base.service.ts` now emits path and param keys only, not the composed URL. +- **CQ-W8** — CORS: startup warning added when `ALLOWED_ORIGINS=*` with non-bearer auth mode. +- **CQ-W7** — HTTP 400 → 404 for session-not-found on three `res.status(400)` sites in `src/lib/http-app.ts` (lines 725, 830, 892 pre-patch). + +**Services** + +- **SEC-1** — API key stored as `sha256(apiKey)` Map key in `src/services/index.ts`; plaintext key no longer retained in process memory after lookup. +- **CQ-C1 / CQ-W1** — `run_` and `test_` prefixes removed from `READ_ONLY_NON_IDEMPOTENT_TOOL_PREFIXES`; `run_prompt_completion` and `test_mcp_server` now correctly carry `readOnlyHint: false`. +- **CQ-W11 / CQ-W12** — Stateless-mode transport no longer receives `eventStore`; `eventStore: undefined` set for stateless transports. +- **CQ-S2** — Shared service map uses LRU eviction; overflow-bucket state capped. +- **PERF-1s / PERF-3** — Redis client import deferred until first use; service-cache lookup short-circuits before constructing a full `PortkeyService`. +- **CQ-S1** — Domain tool files migrated to call `registerTool` directly instead of the `server.tool()` proxy. + +**Tools** + +- **PERF-1t / PERF-2** — Tool response payloads compacted (JSON serialisation, ~157 sites); reduces token consumption per tool call. +- **SEC-3** — `create_api_key` description updated with explicit transcript-exposure warning; the secret is returned once in the tool result and will be visible in MCP transcripts and LLM context. +- **CQ-W2–CQ-W6 / CQ-W9–CQ-W10** — Miscellaneous code-quality and type-safety fixes across tool registration and annotation helpers. +- **CQ-S3 / CQ-S4** — Tool annotation inference and `update_*` idempotency prefix group added. + +**Tests** + +140 tests added across 9 new files covering 13 tool modules, Clerk auth, HTTP session endpoints, and contract schemas for workspaces and users. + +--- + +### Deliberately-accepted items with rationale + +| Item | Decision | Rationale | +|------|----------|-----------| +| Stateless-mode `McpServer` created per request | **Accept** | The MCP SDK binds one transport per server instance. A long-lived server cannot be reused across stateless requests without sharing transport state, which the SDK does not support. Per-request construction is the correct pattern for stateless deployments. | +| Rate-limiter overflow bucket retains state for excess clients | **Accept** | The bucket is now LRU-capped (CQ-S2). Remaining state is bounded in memory and necessary to enforce limits on clients that exceed the normal window. Eliminating all state would allow burst-then-drain evasion. | +| CSP `unsafe-inline` on the status/index page | **Accept** | The inline `