Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
c99f5c2
docs(mcp): add Claude Connector Store readiness plan
andrew-bierman May 23, 2026
cc3f3e4
chore(mcp): bump mcp sdk → 1.29, oauth provider → 0.7, agents → 0.13.2
andrew-bierman May 23, 2026
a06b296
feat(mcp): wrangler env structure, custom domain, version unification…
andrew-bierman May 23, 2026
c347b0a
feat(mcp): RFC 9728 + 8414 well-known metadata, scoped to custom doma…
andrew-bierman May 23, 2026
cc8cb8f
feat(mcp): gate /register with MCP_INITIAL_ACCESS_TOKEN + pre-registe…
andrew-bierman May 23, 2026
8f4c9f9
feat(mcp): Better Auth trusted origins + login CSRF/Origin + CORS on …
andrew-bierman May 23, 2026
9847c39
feat(mcp): scope-based admin gating; remove admin_login + X-PackRat-A…
andrew-bierman May 23, 2026
f7036f1
feat(mcp): packrat_ namespace + tool annotations on every tool (U7)
andrew-bierman May 23, 2026
a0556ed
feat(mcp): structured output + isError envelope + pagination clamps (U8)
andrew-bierman May 23, 2026
731b277
feat(mcp): resources/list providers, search template, packrat://gloss…
andrew-bierman May 23, 2026
7ddd7e1
feat(mcp): elicitations on destructive admin tools (U10)
andrew-bierman May 23, 2026
032f12b
feat(mcp): branded login page + UX polish; SSO deferred (U11)
andrew-bierman May 23, 2026
785b9c7
feat(landing,mcp): Terms of Service, Privacy MCP addendum, /health le…
andrew-bierman May 23, 2026
06bceb4
feat(landing,mcp): public docs page, README, branding, submission pac…
andrew-bierman May 23, 2026
219cfec
feat(mcp): rate-limit binding + per-user/per-tool gating + KV purge c…
andrew-bierman May 23, 2026
96e6bd7
feat(mcp): structured logging + audit logs + onError hook + correlati…
andrew-bierman May 23, 2026
4fe5cff
feat(mcp): real /health probing KV + API; new /status endpoint (U16)
andrew-bierman May 23, 2026
f169e6f
feat(mcp,ci): GH Actions PR test + tag deploy workflows; vitest-pool-…
andrew-bierman May 23, 2026
871b1f0
feat(mcp): submission readiness script + completed submission packet …
andrew-bierman May 23, 2026
640a4ac
chore(landing): pre-render MCP logo PNGs at 1024/512/256 from SVG
andrew-bierman May 25, 2026
ac00500
chore(mcp): wire real OAUTH_KV namespace IDs (prod + dev)
andrew-bierman May 25, 2026
e832842
docs(mcp): fix stale KV-namespace naming refs after live provisioning
andrew-bierman May 25, 2026
8521da1
docs(mcp): add Better Auth OAuth Provider consolidation plan (refacto…
andrew-bierman May 25, 2026
3f048c2
feat(api): @better-auth/oauth-provider plugin + consent page + Claude…
andrew-bierman May 25, 2026
f32f2fc
feat(mcp): JWT validation via remote JWKS for Better Auth tokens (U2)
andrew-bierman May 25, 2026
f0810a7
feat(mcp): cutover to pure protected resource — delete workers-oauth-…
andrew-bierman May 26, 2026
6d309a6
chore(mcp): regenerate lockfile after U3+U4 dep removal
andrew-bierman May 26, 2026
0bd27f6
chore(mcp): strip dead OAUTH_KV + cron + DCR secret; rewrite runbook …
andrew-bierman May 26, 2026
c652f69
test(mcp): retire workers-oauth-provider tests; rewrite fixtures for …
andrew-bierman May 26, 2026
47da19d
chore(mcp,ci): readiness script + workflows for cross-origin AS archi…
andrew-bierman May 26, 2026
2c4930c
docs(mcp): final docs sweep for post-refactor architecture + R11 dev-…
andrew-bierman May 26, 2026
6fcfa3b
🚚 chore(api): relocate Claude OAuth seed to canonical src/db/ location
andrew-bierman May 26, 2026
65cf7ab
♻️ refactor(api): transitional env-var rename BETTER_AUTH_* → PACKRAT_*
andrew-bierman May 26, 2026
ea8e7f7
✨ feat(api): add drizzle-seed for db:seed:dev local fake data
andrew-bierman May 26, 2026
3c1ee01
♻️ refactor(api): modernize consent page — JSX via @kitajs/html + Ely…
andrew-bierman May 26, 2026
5662863
♻️ refactor(api): unify all prod seeders on drizzle-seed pattern
andrew-bierman May 26, 2026
aad354f
🔒 chore(api): drop --force escape in db:seed:dev + sort package.jsons
andrew-bierman May 26, 2026
df22a89
✨ feat(guards): add isBoolean type guard
andrew-bierman May 26, 2026
9a10b47
🔧 fix(mcp): replace raw typeof checks with @packrat/guards
andrew-bierman May 26, 2026
9b0cc69
🔧 fix(mcp,landing): replace unsafe type casts with @packrat/guards
andrew-bierman May 26, 2026
1ecda5d
📌 chore(deps): align jose + @cloudflare/vitest-pool-workers across pa…
andrew-bierman May 26, 2026
d58c33d
🔧 test(mcp): update submission-readiness test to match new db:seed:oa…
andrew-bierman May 26, 2026
1768796
🔀 Merge branch 'origin/development' into plan/mcp-connector-store-rea…
andrew-bierman May 26, 2026
1594391
🎨 chore: biome import-order auto-fixes from prior commit
andrew-bierman May 26, 2026
701cc8c
🔀 Merge branch 'origin/development' (record proper merge history)
andrew-bierman May 26, 2026
ab57f9f
🙈 chore: gitignore .wrangler/ local state (dev/test artifacts)
andrew-bierman May 26, 2026
4577692
fix(mcp,api): conform to SDK 1.29 + jose 6 type tightening from dev m…
andrew-bierman May 30, 2026
a90a51e
fix(mcp): narrow McpToolResult.content, fix nowIso/call/spy/jsx resid…
andrew-bierman May 30, 2026
5a102d3
refactor(api): split JSX-free App contract into app.ts to stop JSX leak
andrew-bierman May 30, 2026
0cda679
fix(schemas,mcp): give Eden proper error types + string-coerce query …
andrew-bierman May 30, 2026
5c986f9
fix(mcp): align tool request bodies with merged API contract
andrew-bierman May 30, 2026
f616adb
fix(mcp): loosen call() to accept Eden's unknown error (revert schema…
andrew-bierman May 30, 2026
3ceaa92
fix(schemas,mcp): type admin error responses explicitly for proper Ed…
andrew-bierman May 30, 2026
63bbf78
fix: revert to z.any() error schema + unknown-tolerant call()
andrew-bierman May 30, 2026
3c171b6
test(api): cover logger Sentry forwarding + exclude app.ts from unit …
andrew-bierman May 30, 2026
a183e18
refactor(mcp): resources.ts owned helpers to single object param (lint)
andrew-bierman May 30, 2026
d68e3b7
refactor(mcp): cors + index.ts owned helpers to object param (lint)
andrew-bierman May 30, 2026
7f0e923
refactor(mcp,api): logger methods + emit to single object param (lint)
andrew-bierman May 30, 2026
7958d7a
refactor(api,scripts): owned helpers to object param (lint)
andrew-bierman May 30, 2026
8f2f696
refactor(mcp): remaining owned helpers to object param + framework al…
andrew-bierman May 30, 2026
1a12169
test(mcp): update submission-readiness.test.ts call sites to object p…
andrew-bierman May 30, 2026
72a1cc0
Merge remote-tracking branch 'origin/development' into plan/mcp-conne…
andrew-bierman May 30, 2026
fd5b9ce
fix(types): isolate api's @kitajs/html JSX from the root tsc program
andrew-bierman May 30, 2026
b225834
fix(api): make api tsconfig self-sufficient by extending root
andrew-bierman May 30, 2026
76bd692
fix(api): load Cloudflare Workers types explicitly in api tsconfig
andrew-bierman May 30, 2026
9bfc3b0
fix(api): use dated @cloudflare/workers-types/latest subpath for glob…
andrew-bierman May 30, 2026
e993654
fix(types): exclude only api's JSX files from root tsc, not all of pa…
andrew-bierman May 30, 2026
fc47660
fix(types): exclude consent-page.test.ts from root tsc (last @kitajs/…
andrew-bierman May 30, 2026
a5bacf8
fix(types): exclude api integration tests from root tsc + fix 6 laten…
andrew-bierman May 30, 2026
1c43a69
fix(api): cfAccess.test.ts — derive KeyLike type (jose 6 dropped the …
andrew-bierman May 30, 2026
7757402
docs(mcp): ADR-0001 — oauth-provider over the bundled mcp() plugin
andrew-bierman May 31, 2026
d9e784a
docs(mcp): add Risk status to ADR-0001
andrew-bierman May 31, 2026
11bc95c
docs(mcp): reframe legacy KV cleanup as optional pre-launch housekeeping
andrew-bierman May 31, 2026
fd52725
docs(mcp): record that there's no legacy KV/secret to clean up (verif…
andrew-bierman May 31, 2026
c35f5c6
feat(api): CodeRabbit triage fixes + Workers-native Sentry observabil…
andrew-bierman Jun 1, 2026
0bba876
Merge remote-tracking branch 'origin/development' into plan/mcp-conne…
andrew-bierman Jun 1, 2026
e245d31
fix(post-merge): resolve type/coverage/dep/lint failures after develo…
andrew-bierman Jun 1, 2026
ae4250f
fix(types): stop worker-configuration.d.ts re-leaking @kitajs/html JS…
andrew-bierman Jun 1, 2026
2e952e5
Merge remote-tracking branch 'origin/development' into plan/mcp-conne…
andrew-bierman Jun 1, 2026
44e7086
fix(post-merge2): pin oauth-provider 1.6.11 + re-enable mcp check-types
andrew-bierman Jun 1, 2026
6f15a02
refactor(consent): extract server HTML into built @packrat/consent-ui…
andrew-bierman Jun 1, 2026
c7112d6
refactor(mcp tests): restore noUncheckedIndexedAccess honesty (drop `…
andrew-bierman Jun 1, 2026
11dd927
ci(mcp): drop the OOMing type-check step (#2533); keep Biome + deploy…
andrew-bierman Jun 1, 2026
25e0e28
♻️ Re-enable MCP type-check via type-erasing tool()/prompt() wrappers…
andrew-bierman Jun 1, 2026
0498510
🐛 Exempt MCP tool()/prompt() wrappers from no-owned-max-params
andrew-bierman Jun 1, 2026
645709e
Merge origin/development into plan/mcp-connector-store-readiness
andrew-bierman Jun 2, 2026
6996020
✅ test(mcp): cover registerPrompts + prompt() wrapper
andrew-bierman Jun 2, 2026
ce46842
✅ test(mcp): real handler/helper coverage for the MCP surface (74% → …
andrew-bierman Jun 2, 2026
f4a6907
✅ test(mcp): cover error/optional branches → ratchet baseline met (98…
andrew-bierman Jun 2, 2026
f8d326f
✅ test(api): cover consent-route scope/role branches (81% → 94% branch)
andrew-bierman Jun 2, 2026
90c7430
✅ test(api): restore branch coverage on merge-touched files (ratchet)
andrew-bierman Jun 2, 2026
cc93e91
🐛 test(mcp): fix tools-admin-handlers tsc — typed structured-error ac…
andrew-bierman Jun 2, 2026
ec7830a
♻️ chore(mcp): drop mcp-deploy.yml; deploy CF-native + version_metadata
andrew-bierman Jun 4, 2026
5beb6a1
fix(mcp): address connector readiness review cleanup
andrew-bierman Jun 5, 2026
0691910
chore(mcp): remove redundant dedicated test workflow
andrew-bierman Jun 5, 2026
d9b047b
chore(mcp): remove manual readiness workflow
andrew-bierman Jun 5, 2026
a30653d
fix(api-client): keep app route type behind client package
andrew-bierman Jun 5, 2026
4e50e2d
fix(mcp): remove unlaunched umbrella scope
andrew-bierman Jun 5, 2026
929cf56
👷 ci(checks): gate types two ways — root tsc + per-package turbo
andrew-bierman Jun 6, 2026
3ce4fc0
🔒️ ci(types): every package/app now type-checks (28/28)
andrew-bierman Jun 6, 2026
00c3228
Merge origin/development into plan/mcp-connector-store-readiness
andrew-bierman Jun 12, 2026
df77200
Fix MCP service version metadata
andrew-bierman Jun 12, 2026
1ad09ca
Remove direct Expo API package dependency
andrew-bierman Jun 12, 2026
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
15 changes: 14 additions & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,21 @@ jobs:
run: bun scripts/lint/no-unauth-routes.ts
- name: Check unsafe type casts
run: bun check:casts:strict
- name: Check types
# Two type-check passes, deliberately. `bun check-types` runs a single
# root `tsc` over the shared React-Native/web/node program (jsx:
# react-native, lib DOM+ESNext). That program structurally CANNOT house
# packages with a different global type environment — Cloudflare Workers
# packages (packages/mcp: @cloudflare/workers-types, no DOM) and the
# @kitajs/html JSX package (packages/consent-ui: global JSX namespace) —
# so those, plus osm-db/osm-import/overpass, are excluded from the root
# tsconfig. `bun check-types:packages` (turbo) then type-checks EVERY
# package under its OWN tsconfig, closing that gap. Both must stay: the
# root pass catches cross-package program errors, the turbo pass catches
# per-package errors the root program can't see (e.g. MCP, #2533).
- name: Check types (root tsc — shared RN/web/node program)
run: bun check-types
- name: Check types (per-package — turbo, each tsconfig)
run: bun check-types:packages
- name: Run Expo Doctor
run: bunx expo-doctor
working-directory: apps/expo
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,7 @@ apps/guides/public/og/
# Git worktrees
.worktrees/
.worktrees

# Cloudflare wrangler local state (dev/test artifacts)
.wrangler/
.turbo/
41 changes: 23 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,28 +158,33 @@ Sentry.addBreadcrumb({ category: 'feature', message: 'Action started', level: 'i
- **Better Auth errors** (plain objects with `{ message, status, code }`) are not JS Errors. Use `toAuthError` from `expo-app/features/auth/lib/authErrors` to convert them into an `AuthClientError` that carries `status` and `code`. Capture and throw that — do not create a separate synthetic error for Sentry and another for throwing.
- Include `httpStatus` and `errorCode` in `extra` for any HTTP error so they're searchable in Sentry.

**API / Cloudflare Workers** — use helpers from `@packrat/api/utils/sentry`:
**API / Cloudflare Workers** — helpers from `@packrat/api/utils/sentry`. There are **three boundaries by where code runs** — match the tier to the situation instead of wrapping everything in try/catch:

```ts
import { apiAddBreadcrumb, captureApiException } from '@packrat/api/utils/sentry';
1. **Route handlers → do nothing.** Elysia's global `.onError` (`src/app.ts`) is the central sink: it reports unexpected errors (skips `VALIDATION`/`PARSE`/`NOT_FOUND`), tagged by the matched **route template** + `request_id`. Let errors propagate — don't add a try/catch *just* to report. Only catch in a route to **translate** an error into a specific response (and then it's a swallow — see tier 3).

// Breadcrumb before significant async steps
apiAddBreadcrumb({ category: 'feature', message: 'Fetching external data', level: 'info' });
2. **Sub-operations you rethrow from → wrap in `record`.** Workflow `step.do` bodies, queue/cron consumers, and services called outside an Elysia request. It opens a Sentry span (OTel-semantic, Workers-native) **and** captures-with-context **and** rethrows:

// In every catch block
} catch (error) {
captureApiException(error, {
operation: 'featureName.action',
userId,
tags: { feature: 'myFeature' },
extra: { relevantId },
});
throw error; // or return an error response
}
```
```ts
import { record } from '@packrat/api/utils/sentry';

await record({ operation: 'etl.processLogsBatch', extra: { jobId } }, async () => {
await db.insert(logs).values(rows);
});
```

3. **Catches that swallow → call `captureApiException`** (object signature). Fail-closed `return false`, best-effort metrics, per-item loops that continue, route catches that return an error response:

```ts
} catch (error) {
captureApiException({ error, operation: 'verifyAdmin', extra: { userId } });
return false; // swallowed — nothing to rethrow
}
```

- Use `captureApiException` (not raw `captureException`) — it wraps the call with structured operation context and also logs to console for wrangler dev output.
- Every route `catch` block and service method that interacts with the DB or an external API must have a `captureApiException` call.
- Capture is **idempotent + deduped** (a marker is stamped on the error), so `record`/`captureApiException` + the outer boundary (`.onError`, `withSentry`, `instrumentWorkflowWithSentry`) never double-report — enrich-and-rethrow is always safe.
- Every event in a request shares a **`request_id`** tag (`cf-ray`, set in `.onRequest`), echoed in the `X-Request-Id` response header — pivot on it to tie an `.onError` report to the granular `record`/`captureApiException` events. Sentry's automatic `trace_id` correlates them too.
- Don't use raw `captureException` — the wrappers add operation context + console logging. Include `httpStatus`/`errorCode` in `extra` for HTTP errors; breadcrumb significant steps with `apiAddBreadcrumb`.
- **Not** `@elysiajs/opentelemetry`: its Node OTel SDK doesn't run on workerd (BatchSpanProcessor, AsyncHooks). `record` gives the same `record(name, fn)` ergonomics on Sentry's Workers-native tracer.

### API Client (`@packrat/api-client`)

Expand Down
2 changes: 1 addition & 1 deletion apps/admin/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { treaty } from '@elysiajs/eden';
import type { App } from '@packrat/api';
import type { App } from '@packrat/api-client';
import { isObject } from '@packrat/guards';
import type {
ActiveUsers,
Expand Down
1 change: 0 additions & 1 deletion apps/admin/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"@packrat/app": ["../../packages/app/src/index.ts"],
"@packrat/app/*": ["../../packages/app/src/*"],
"admin-app/*": ["./*"],
"@packrat/api/*": ["../../packages/api/src/*"],
"@packrat/guards": ["../../packages/guards/src"],
"@packrat/guards/*": ["../../packages/guards/src/*"],
"@packrat/web-ui": ["../../packages/web-ui/src"],
Expand Down
1 change: 0 additions & 1 deletion apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"@gorhom/bottom-sheet": "^5.1.2",
"@legendapp/state": "^3.0.0-beta.30",
"@packrat-ai/nativewindui": "2.0.6",
"@packrat/api": "workspace:*",
"@packrat/api-client": "workspace:*",
"@packrat/config": "workspace:*",
"@packrat/constants": "workspace:*",
Expand Down
2 changes: 0 additions & 2 deletions apps/expo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
"baseUrl": ".",
"paths": {
"expo-app/*": ["./*"],
"@packrat/api": ["../../packages/api/src/index.ts"],
"@packrat/api/*": ["../../packages/api/src/*"],
"@packrat/api-client": ["../../packages/api-client/src/index.ts"],
"@packrat/api-client/*": ["../../packages/api-client/src/*"]
}
Expand Down
1 change: 0 additions & 1 deletion apps/expo/vitest.types.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export default defineConfig({
resolve: {
alias: {
'expo-app': resolve(__dirname, '.'),
'@packrat/api': resolve(__dirname, '../../packages/api/src/index.ts'),
'@packrat/api-client': resolve(__dirname, '../../packages/api-client/src/index.ts'),
},
},
Expand Down
2 changes: 1 addition & 1 deletion apps/guides/lib/enhanceGuideContent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { openai } from '@ai-sdk/openai';
import { treaty } from '@elysiajs/eden';
import type { App } from '@packrat/api';
import type { App } from '@packrat/api-client';
import { guideEnv } from '@packrat/env/next';
import { generateText, tool } from 'ai';
import { z } from 'zod';
Expand Down
2 changes: 1 addition & 1 deletion apps/guides/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@ai-sdk/openai": "catalog:",
"@elysiajs/eden": "catalog:",
"@hookform/resolvers": "catalog:",
"@packrat/api": "workspace:*",
"@packrat/api-client": "workspace:*",
"@packrat/env": "workspace:*",
"@packrat/guards": "workspace:*",
"@packrat/schemas": "workspace:*",
Expand Down
123 changes: 123 additions & 0 deletions apps/landing/__tests__/legal.pages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Smoke tests for the legal pages (U12 of the MCP connector-store readiness
* plan).
*
* The landing app uses Next.js with `output: 'export'` and a node-environment
* vitest setup (see `vitest.config.ts`). React-server-component imports don't
* resolve cleanly in that env, so the route-level "GET returns 200 with this
* string" test the og-meta suite performs (against the built `out/` HTML)
* would be the only true smoke pattern. We don't run a full Next build inside
* this suite to keep it fast; instead, the assertions below operate on the
* source `.tsx` files for the two legal pages and on the shared
* `config/site.ts` block that wires them up.
*
* What we verify:
* - The Terms of Service page source exists, exports the standard metadata
* shape, and contains the load-bearing MCP, jurisdiction-TODO, and
* hello@packratai.com strings a reviewer will scan for.
* - The Privacy Policy page source contains the new MCP / connectors
* addendum (heading + key bullet content).
* - `siteConfig.footerLinks.legal` exposes BOTH Privacy and Terms, and
* `siteConfig.support` advertises the canonical mailto.
*
* If a route smoke pattern lands later (e.g. happy-dom env + RSC eval, or
* a separate `vitest --config out-export.config.ts` workspace), the
* file-text assertions can be replaced — the reviewer-facing intent is the
* stable contract.
*/
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { siteConfig } from '../config/site';

const APP_DIR = resolve(__dirname, '..');
const TOS_PAGE = resolve(APP_DIR, 'app/terms-of-service/page.tsx');
const PRIVACY_PAGE = resolve(APP_DIR, 'app/privacy-policy/page.tsx');

describe('Terms of Service page (/terms-of-service)', () => {
it('source file exists', () => {
expect(existsSync(TOS_PAGE)).toBe(true);
});

const source = existsSync(TOS_PAGE) ? readFileSync(TOS_PAGE, 'utf8') : '';

it('exports a Next.js metadata block with title/description/robots', () => {
expect(source).toContain('export const metadata');
expect(source).toContain("title: 'Terms of Service | PackRat'");
expect(source).toMatch(/description:\s*'[^']+'/);
expect(source).toMatch(/robots:\s*\{[^}]*index:\s*true/);
});

it('renders the "Terms of Service" heading', () => {
expect(source).toContain('>Terms of Service<');
});

it('covers MCP connector provisions', () => {
// Reviewers grep for "MCP" — this section is the new content this unit
// ships and is what Anthropic's policy expects to find.
expect(source).toMatch(/MCP/);
expect(source).toContain('mcp.packratai.com');
expect(source).toMatch(/mcp:admin/);
expect(source).toMatch(/OAuth/);
});

it('includes the outdoor-safety disclaimer', () => {
expect(source).toMatch(/inherent risks/i);
});

it('surfaces the canonical support contact', () => {
expect(source).toContain('hello@packratai.com');
});

it('leaves the operator-jurisdiction TODO marker in place', () => {
// U12 deliberately ships with a placeholder jurisdiction (Delaware) and a
// TODO so the operator can replace it after legal review. The check
// prevents the TODO from being silently lost in a future edit.
expect(source).toMatch(/TODO\(operator\): set jurisdiction/);
});
});

describe('Privacy Policy page (/privacy-policy) — MCP addendum', () => {
it('source file exists', () => {
expect(existsSync(PRIVACY_PAGE)).toBe(true);
});

const source = existsSync(PRIVACY_PAGE) ? readFileSync(PRIVACY_PAGE, 'utf8') : '';

it('renders the new "MCP Connector & Third-Party Clients" section heading', () => {
expect(source).toContain('MCP Connector & Third-Party Clients');
});

it('explains OAuth token storage and rotation', () => {
expect(source).toMatch(/refresh token/i);
expect(source).toMatch(/Cloudflare KV/);
expect(source).toMatch(/60 minutes/);
});

it('clarifies what MCP clients do NOT see', () => {
expect(source).toMatch(/never sees your\s+PackRat password|never sees your password/i);
expect(source).toMatch(/conversation content/i);
});

it('points users at hello@packratai.com for deletion', () => {
expect(source).toContain('hello@packratai.com');
});
});

describe('siteConfig wiring (U12)', () => {
it('exposes BOTH Privacy and Terms in the footer legal block', () => {
const titles = siteConfig.footerLinks.legal.map((l) => l.title);
expect(titles).toContain('Privacy');
expect(titles).toContain('Terms');

const hrefs = siteConfig.footerLinks.legal.map((l) => l.href);
expect(hrefs).toContain('/privacy-policy');
expect(hrefs).toContain('/terms-of-service');
});

it('exposes the canonical support contact', () => {
expect(siteConfig.support).toBeDefined();
expect(siteConfig.support.email).toBe('hello@packratai.com');
expect(siteConfig.support.mailto).toBe('mailto:hello@packratai.com');
Comment on lines +42 to +121

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

These assertions are tied to source text, not page behavior.

Reading .tsx files and grepping literals will miss regressions in the rendered legal pages and metadata contract, while also failing on harmless refactors like extracting shared constants. Please move this to a render/build-level check of the actual page output.

As per coding guidelines "Avoid testing implementation details; test observable behaviour."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/landing/__tests__/legal.pages.test.ts` around lines 42 - 121, The tests
in legal.pages.test.ts read TOS_PAGE and PRIVACY_PAGE files and assert on source
literals (TOS_PAGE, PRIVACY_PAGE, siteConfig.footerLinks), which couples tests
to implementation text; replace those file-grepping assertions with
render/build-level checks by importing the page components (the default exports
for the Terms and Privacy pages) or using Next's server-side render utilities
(e.g., renderToStaticMarkup or `@testing-library/react/server`) to produce HTML,
then assert on the rendered output and metadata (title/description/robots) and
visible headings/phrasing (e.g., "Terms of Service", "MCP Connector &
Third-Party Clients", "inherent risks", "refresh token", "Cloudflare KV")
instead of matching source file strings; leave the siteConfig assertions for
footer wiring (siteConfig.footerLinks.legal and siteConfig.support) as-is or
verify they render in the footer via the rendered layout.

});
});
131 changes: 131 additions & 0 deletions apps/landing/__tests__/mcp.page.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Smoke tests for the MCP public docs page (U13).
*
* Same vitest-against-source approach as `legal.pages.test.ts`: the landing
* app uses Next.js `output: 'export'` and a node-only vitest env, so we
* can't import the RSC route directly. Assertions operate on the page
* source plus the generated `mcp-catalog.json` to verify reviewer-facing
* invariants the connector-store submission will be evaluated against.
*
* What we verify:
* - The page source exists, exports the standard metadata shape with
* `robots.index: true` (Anthropic must be able to crawl the docs URL).
* - The Quickstart / Scopes / Example prompts / Tool catalog / Resources
* / Privacy & security / Reviewer test account sections all render.
* - Three example prompts appear (per Software Directory Policy).
* - The Claude.ai custom-connector install URL is exactly the production
* MCP endpoint string.
* - `mcp-catalog.json` is present and non-trivial — the page renders
* from it, so a missing or empty JSON would surface as a build-time
* RSC error in production.
*/

import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

const APP_DIR = resolve(__dirname, '..');
const PAGE = resolve(APP_DIR, 'app/mcp/page.tsx');
const CATALOG = resolve(APP_DIR, 'data/mcp-catalog.json');

describe('MCP public docs page (/mcp)', () => {
it('source file exists', () => {
expect(existsSync(PAGE)).toBe(true);
});

const source = existsSync(PAGE) ? readFileSync(PAGE, 'utf8') : '';

it('exports a Next.js metadata block (indexable)', () => {
expect(source).toContain('export const metadata');
expect(source).toMatch(/title:\s*'PackRat MCP Connector \| PackRat'/);
expect(source).toMatch(/robots:\s*\{\s*index:\s*true/);
});

it('renders the hero heading', () => {
expect(source).toMatch(/Plan trips, build packs, check weather/);
});

it('exposes the production MCP endpoint URL verbatim', () => {
// The submission packet, the public docs page, and the worker's
// resourceMetadata MUST all advertise the same URL. A diff here is the
// canary on a drift that breaks Anthropic's audience verification.
expect(source).toContain('https://mcp.packratai.com/mcp');
});

it('lists the three OAuth scopes', () => {
// Sourced from the JSON dump at render time, but the header / table
// copy refers to them inline; the smoke test asserts both.
for (const scope of ['mcp:read', 'mcp:write', 'mcp:admin']) {
expect(source).toContain(scope);
}
});

it('uses the Anthropic "custom connector" terminology', () => {
expect(source).toMatch(/custom connector/i);
});

it('ships ≥ 3 example prompts (Software Directory Policy)', () => {
// Each example prompt is wrapped in a <blockquote>; count those.
const blockquotes = source.match(/<blockquote/g) ?? [];
expect(blockquotes.length).toBeGreaterThanOrEqual(3);
});

it('points reviewers at the submission-packet doc for credentials', () => {
expect(source).toContain('docs/mcp/submission-packet.md');
// And explicitly states credentials are NOT on the public page.
expect(source).toMatch(/do not publish credentials/i);
});

it('links the legal / privacy / support surfaces', () => {
expect(source).toContain('/privacy-policy');
expect(source).toContain('/terms-of-service');
expect(source).toContain('hello@packratai.com');
});

it('points to the developer README and the implementation plan', () => {
expect(source).toContain('packages/mcp/README.md');
expect(source).toContain(
'docs/plans/2026-05-22-001-feat-mcp-connector-store-readiness-plan.md',
);
expect(source).toContain('docs/mcp/runbook.md');
});
});

describe('mcp-catalog.json (build-time data source for /mcp)', () => {
it('is present and parses as JSON', () => {
expect(existsSync(CATALOG)).toBe(true);
const raw = readFileSync(CATALOG, 'utf8');
// Must round-trip cleanly — the page imports it as a typed module.
expect(() => JSON.parse(raw)).not.toThrow();
});

const raw = existsSync(CATALOG) ? readFileSync(CATALOG, 'utf8') : '{}';
const catalog = JSON.parse(raw) as {
totalTools?: number;
counts?: { byClassification?: Record<string, number> };
tools?: Array<{ name: string; classification: string }>;
endpoint?: string;
};

it('contains ≥ 80 tools (sanity floor matching the U7 annotations test)', () => {
expect(catalog.totalTools ?? 0).toBeGreaterThanOrEqual(80);
expect((catalog.tools ?? []).length).toBe(catalog.totalTools ?? -1);
});

it('every tool name starts with the packrat_ prefix', () => {
for (const t of catalog.tools ?? []) {
expect(t.name).toMatch(/^packrat_/);
}
});

it('partitions tools into read / write / admin classifications', () => {
const c = catalog.counts?.byClassification ?? {};
expect(c.read).toBeGreaterThan(0);
expect(c.write).toBeGreaterThan(0);
expect(c.admin).toBeGreaterThan(0);
});

it('advertises the production endpoint URL', () => {
expect(catalog.endpoint).toBe('https://mcp.packratai.com/mcp');
});
});
Loading
Loading