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
62 changes: 62 additions & 0 deletions .github/workflows/auto-tag.yml
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
74 changes: 73 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion ENDPOINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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).
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -202,7 +202,7 @@ For local-only HTTP use, leave `MCP_HOST` at its default `127.0.0.1`. Set `MCP_H
<details>
<summary><strong>Vercel deployment</strong></summary>

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
Expand Down
10 changes: 10 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
68 changes: 40 additions & 28 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion docs/VERCEL_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading