# SkillFoundry v5.14.0 — MCP Server Security Hardening

**Released:** 2026-05-13

---

## What's in This Release

v5.14.0 is a security hardening release for the MCP server. No new features, no pipeline changes — every change closes a gap identified in a full BPSBS audit. All 11 BPSBS security controls now pass. The framework's own security standards are now reflected in the server that runs it.

---

## Breaking Change — Bearer Token Auth

**MCP clients must now include an `Authorization` header.**

On first boot, the server auto-generates a token and prints the ready-to-paste config:

```
[auth] New API token generated and saved to: .../data/.api-token

[auth] API token: sf_a3f9...
  Token file: .../data/.api-token

To connect from Claude Code, add to settings.json:
  "mcpServers": {
    "skillfoundry": {
      "url": "http://localhost:9877/mcp/sse",
      "headers": { "Authorization": "Bearer sf_a3f9..." }
    }
  }
```

The token is stable across restarts (read from `data/.api-token` on subsequent boots). `/health` and `/ready` remain public for monitoring.

**Rotation:** delete `data/.api-token` and restart, or set `SKILLFOUNDRY_API_TOKEN=<new-token>` in your environment.

---

## Security Changes

### Bearer Token Auth (`server.ts`)

`resolveApiToken()` implements a three-level priority chain:

1. `SKILLFOUNDRY_API_TOKEN` env var (CI / Docker override)
2. `data/.api-token` file (persisted from previous run)
3. Generate `sf_<64-hex-chars>` via `crypto.randomBytes(32)`, write with `chmod 0600`

Auth middleware applied to every request on `/mcp/*` and `/api/v1/*`. `/health` and `/ready` remain public.

### Rate Limiting (`server.ts`)

`express-rate-limit` with separate buckets per transport:

| Path | Limit |
|------|-------|
| `/mcp` | 300 req / 60 s |
| `/api` | 500 req / 60 s |

Standard `RateLimit-*` headers returned; legacy `X-RateLimit-*` headers disabled.

### CORS (`server.ts`)

Explicit `cors()` middleware. Default origin: `http://localhost:3666`. Override:

```bash
SKILLFOUNDRY_CORS_ORIGIN=https://your-dashboard.example.com
```

### Error Response Sanitisation (`api/routes.ts`)

`handleRouteError()` replaces 8 identical catch blocks that previously returned `(err as Error).message` directly in HTTP 500 responses — leaking SQL errors, file paths, and exception internals.

| `NODE_ENV` | 500 message |
|------------|-------------|
| `development` | Real `err.message` |
| anything else | `"An internal error occurred"` |

### `MAX_PAGE_SIZE = 500` (`state/db.ts`)

Enforced on every list query. Caller-supplied `limit` is capped at 500 via `Math.min(caller, MAX_PAGE_SIZE)`. Queries with no caller-supplied limit default to `MAX_PAGE_SIZE`.

Affected functions: `queryQuirks`, `getCertifiedSkills`, `listDynamicSkills`, `getFleetHealth`, `querySessionRecordings`, `getSessionTranscripts`, `queryPlatformInsights`, `queryProjectArtifacts`, `getDeviationRules`, `getCorrectionPatterns`.

### `getSessionTranscripts` Mapper (`state/db.ts`)

Previously returned `db.prepare(...).all() as SessionTranscriptRecord[]` — a direct TypeScript cast that doesn't strip columns at runtime. The DB schema includes `id` and `parsed_at` columns not in the interface; those were silently present in every returned object and would appear in any JSON serialisation.

`rowToSessionTranscript()` now explicitly maps only the 13 typed fields, stripping all DB-internal columns at the boundary.

### Atomic Session Writes (`session/persistence.ts`)

`saveSession` previously called `writeFileSync(filePath, data)` directly. An interrupted write (process crash, SIGKILL, disk full) left a truncated file that silently returned `null` on the next `loadSession` call.

New pattern: write to `sessionId.json.tmp`, then `renameSync(tmp, target)`. On POSIX systems `rename(2)` is atomic — either the full new content is visible, or the previous file is untouched.

---

## Test Suite

| | Before | After |
|-|--------|-------|
| Test files | 8 | 8 |
| Tests | 60 (1 failing) | 64 (64 passing) |
| Auth tests | 0 | 3 |

**New negative auth tests** (`mcp-connection.test.ts`):
- `GET /api/v1/agents` with no token → 401
- `GET /api/v1/agents` with wrong token → 401
- `GET /health` with no token → 200 (public)

**Fixed tests:**
- Stale `expect(data.status).toBe("ok")` — health endpoint returns `"starting"` or `"healthy"`, never `"ok"`
- Stale `expect(data.skills)` — field is `data.tools.registered`
- All three server-spawning test files now inject `SKILLFOUNDRY_API_TOKEN` and pass auth headers via `SSEClientTransport`'s `requestInit.headers`

---

## Dependencies Added

| Package | Version | Role |
|---------|---------|------|
| `cors` | `^2.8.6` | CORS middleware |
| `express-rate-limit` | `^8.5.1` | Rate limiting |
| `@types/cors` | `^2.8.x` | TypeScript types (devDep) |

---

## Upgrade Notes

1. **Update your MCP client config** — Add `"headers": { "Authorization": "Bearer <token>" }` to every `mcpServers` entry pointing at this server. The token is at `data/.api-token` and printed on startup.

2. **CI / Docker** — Set `SKILLFOUNDRY_API_TOKEN=<token>` in your environment to pin the token instead of reading from the file.

3. **Custom CORS origin** — If your web UI runs on a port other than 3666, set `SKILLFOUNDRY_CORS_ORIGIN=http://localhost:<port>`.

4. **`mcp-server/package-lock.json` updated** — Run `npm ci` in `mcp-server/` after pulling.

---

## BPSBS Compliance After This Release

| Control | v5.13.0 | v5.14.0 |
|---------|---------|---------|
| No hardcoded secrets | ✅ | ✅ |
| No stack traces in responses | ❌ | ✅ |
| Auth on protected endpoints | ❌ | ✅ |
| Rate limiting | ❌ | ✅ |
| Input validation & size limits | 🟡 | ✅ |
| Concurrent modification safety | 🟡 | ✅ |
| Error leakage prevention | ❌ | ✅ |
| Testing — negative + boundary | 🟡 | ✅ |
| Data isolation | ✅ | ✅ |
| Documentation | ✅ | ✅ |

---

## Files Changed

| File | Change |
|------|--------|
| `mcp-server/src/server.ts` | `resolveApiToken()`, CORS, rate limiting, bearer auth middleware |
| `mcp-server/src/api/routes.ts` | `handleRouteError()` utility replacing 8 identical catch blocks |
| `mcp-server/src/state/db.ts` | `MAX_PAGE_SIZE = 500`, `rowToSessionTranscript()` mapper |
| `mcp-server/src/session/persistence.ts` | Atomic write via `.tmp` + `renameSync` |
| `mcp-server/tests/mcp-connection.test.ts` | Auth token wired, 3 negative tests added, stale assertions fixed |
| `mcp-server/tests/tool-agents.test.ts` | `mkTransport()` helper with auth headers |
| `mcp-server/tests/tool-agents-tier123.test.ts` | `mkTransport()` helper with auth headers |
| `mcp-server/package.json` | `cors`, `express-rate-limit`, `@types/cors` added |

---

## Version

```
5.13.0 → 5.14.0 (MINOR — breaking change for MCP clients: auth header required)
```
