diff --git a/.github/workflows/mcp-harness.yml b/.github/workflows/mcp-harness.yml new file mode 100644 index 0000000000..8bf271ec96 --- /dev/null +++ b/.github/workflows/mcp-harness.yml @@ -0,0 +1,75 @@ +name: MCP harness + +# Integration gate for @posthog/mcp: real MCP servers over real HTTP, on both SDK +# majors and both protocol eras, plus the NestJS/@rekog/mcp-nest customer stacks. +# The harness lives at packages/mcp/harness/ and needs no secrets — the PostHog +# client is an in-process recorder — so it runs on fork PRs too. + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + affected: + uses: ./.github/workflows/check-affected.yml + with: + package-name: '@posthog/mcp' + # The harness lives inside packages/mcp, so the turbo filter already covers + # it. What turbo cannot see is the gate's own machinery: list it, or a + # change that breaks how "affected" is computed skips this workflow and + # reports green. + additional-paths: | + .github/workflows/mcp-harness.yml + .github/workflows/check-affected.yml + .github/actions/is-affected/action.yaml + .github/actions/setup/action.yaml + + harness: + name: ${{ matrix.lane.name }} + needs: affected + if: needs.affected.outputs.is-affected == 'true' + runs-on: ubuntu-22.04 + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + lane: + - { name: 'official SDK v1', script: 'test:integration:sdk-v1' } + - { name: 'official SDK v2', script: 'test:integration:sdk-v2' } + - { name: 'mcp-nest (SDK v1)', script: 'test:integration:nest-v1' } + - { name: 'mcp-nest (SDK v2)', script: 'test:integration:nest-v2' } + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup environment + uses: ./.github/actions/setup + with: + build: false + + # Scoped build: @posthog/mcp plus its workspace dependency closure + # (@posthog/types, @posthog/core, posthog-node — needed for its .d.ts + # build), not the whole workspace. A package outside that closure cannot + # take the gate down by failing to build. + - name: Build @posthog/mcp + run: pnpm exec turbo run build --filter=@posthog/mcp + + - name: Run lane + run: | + set -o pipefail + pnpm --filter @posthog/mcp run ${{ matrix.lane.script }} 2>&1 | tee harness.log + + - name: Upload harness log + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() + with: + name: mcp-harness-${{ strategy.job-index }} + path: harness.log + retention-days: 14 diff --git a/package.json b/package.json index 530828c52c..c475ac2542 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:docs-scripts": "node --test \"scripts/docs/__tests__/*.test.js\"", "test:functional": "turbo test:functional", "test:e2e": "turbo test:e2e", + "test:mcp-harness": "node packages/mcp/harness/run-all.mjs", "build": "turbo build", "dev": "turbo dev", "package": "export PACKAGE_DEST=$(pwd)/target && turbo package", diff --git a/packages/mcp/harness/README.md b/packages/mcp/harness/README.md new file mode 100644 index 0000000000..7a69485368 --- /dev/null +++ b/packages/mcp/harness/README.md @@ -0,0 +1,148 @@ +# @posthog/mcp integration harness + +Real MCP servers over real HTTP, instrumented with the workspace build of `@posthog/mcp` — the +integration gate behind the `MCP harness` CI workflow +([`.github/workflows/mcp-harness.yml`](../../../.github/workflows/mcp-harness.yml)), which blocks +every PR that affects `@posthog/mcp`. + +The unit suite proves unit contracts; this harness proves that a real server, over real HTTP, on +either SDK major, still reports correctly. Between the lanes it covers the five things the package +exists to capture: **tool calls, errors, intent, sessions (conversation-id on and off), and the +NestJS customer stacks**. + +## Running it + +```bash +pnpm test:mcp-harness # from the repo root: builds @posthog/mcp, runs all four lanes +``` + +Or one lane at a time — exactly what CI runs, one job per lane: + +```bash +pnpm --filter @posthog/mcp run test:integration:sdk-v1 # "official SDK v1" +pnpm --filter @posthog/mcp run test:integration:sdk-v2 # "official SDK v2" +pnpm --filter @posthog/mcp run test:integration:nest-v1 # "mcp-nest (SDK v1)" +pnpm --filter @posthog/mcp run test:integration:nest-v2 # "mcp-nest (SDK v2)" +``` + +No secrets, no network: the PostHog client is an in-process recorder +(`dual-era/shared/posthog.mjs`, `nest-*/src/posthog.ts`) that serves recorded events back over +`/__events`. The harness runs on fork PRs. + +Fixtures import `@posthog/mcp` through Node self-reference, which resolves to +`packages/mcp/dist` — they always test the workspace build. Rebuild after changing `src/` +(`pnpm test:mcp-harness` does this for you; for a single lane run +`pnpm exec turbo run build --filter=@posthog/mcp` first). + +## What runs + +| Lane | Path | Covers | +| ---------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `official SDK v1` | `dual-era/matrix.mjs --major v1` | 4 rows: high/low-level instrumentation × stateful/stateless, legacy era | +| `official SDK v2` | `dual-era/matrix.mjs --major v2` | 10 rows: high/low × 2025/2026 era × conversation-id on/off (per-request), plus two long-lived-server control rows (legacy era only — the long-lived transport tops out at 2025-11-25) | +| each SDK lane | `dual-era/probe-late-handlers.mjs --major v1\|v2` | handlers registered _after_ `instrument()` — the mcp-nest/adapter ordering. Each lane runs only its own major's half, so a red probe names the stack | +| `official SDK v2` only | `dual-era/probe-first-call-error.mjs` | the **first** call of a conversation fails, with `conversation_id` on (v2-only by construction: on v1 the thrown error is captured before the appended result, so the bug is unreachable) | +| each SDK lane | `dual-era/probe-pagination.mjs --major v1\|v2` | a **two-page** tool catalogue: `nextCursor`, `ttlMs`, `cacheScope` and result `_meta` survive the listing wrapper | +| `mcp-nest (SDK v1)` | `nest-v1/verify.mjs` | NestJS + `@rekog/mcp-nest` 1.9 + SDK v1, stateless — 16 assertions × `LEVEL=high\|low` | +| `mcp-nest (SDK v2)` | `nest-v2/verify.mjs` | NestJS + `@rekog/mcp-nest` 2.0 + SDK v2, stateless, both eras — 37 assertions × `LEVEL=high\|low` | + +`LEVEL=high` is `instrument(server)`, as documented. `LEVEL=low` is `instrument(server.server)`, +the workaround users adopted before the compatibility gate opened. Both run. + +**The matrix answers "does the SDK behave?". The Nest harnesses answer "does the customer's stack +work?"** A change can be green on one and broken on the other, which is why both run — the matrix +has no NestJS row (see [#4449](https://github.com/PostHog/posthog-js/issues/4449)). + +The matrix asserts ten columns per row, grouped as capture (`calls`, `errors`, `intent`, +`schema`), identity (`session`, `client`, `protocol`) and safety (`warnings`, `header`, `alive`). + +## Expected failures: exact-match snapshots + +Known-broken cells are pinned in `dual-era/expected-failures.json` and +`nest-v2/expected-failures.json`, each entry carrying a `why`. A run passes iff the failing set +**exactly matches** the file: + +- a new failure prints `regressed: …` and exits non-zero; +- a fixed one prints `now passing — remove from expected-failures.json: …` and _also_ exits + non-zero, so improvements are ratcheted in deliberately; +- a row that reports nothing — server never booted, client died mid-run — prints + `no verdict: …` with the crash dump and exits non-zero. Silence is never a pass: an absent + assertion renders as `·` (not applicable), so a crashed client would otherwise read green. + +A floor ("≥35 of 37") would let a regression hide behind a coincidental improvement. Every cell +that moves needs a reason, and the diff of the snapshot file is where the reason lives. + +Currently pinned: the four `v2 … 2025` `client` cells (`clientInfo` cannot reach a per-request +instance on the v2 SDK's legacy leg — documented limitation), the two `v2 low … conv=on` +`session` cells (parked: no tool registry to read ownership from on the low-level path), and +nest-v2's `error message is clean` on both eras (NestJS's `RpcExceptionsHandler` flattens every +thrown error to `"Internal server error"` — adapter behaviour, not ours). + +Standing regression assertions on every PR: the four `v1` matrix rows stay all-green, and +`nest v1` stays **16/16** on both levels. + +## Running one cell + +Servers bind ephemeral ports by default (`PORT=0`) and announce the chosen port on stdout as +`MCP_HARNESS_LISTENING port=`. Set `PORT` explicitly to pin one: + +```bash +cd packages/mcp/harness/dual-era +PORT=3222 LEVEL=high CONVERSATION_ID=1 node servers/v2.mjs & +node client/run.mjs --url http://localhost:3222 --sdk v2 --lane 2026 --conv on +``` + +| Env / flag | Meaning | +| ------------------- | ------------------------------------------------------------------------ | +| `PORT` | explicit port; default 0 = ephemeral | +| `LEVEL=high\|low` | high-level `McpServer` or bare low-level `Server` | +| `MODE=` | v1: `stateful`/`stateless` · v2: `perrequest`/`longlived` | +| `CONVERSATION_ID=1` | turn on `enableConversationId` (off by default) | +| `CUSTOM_3ARG=1` | register a custom method via v2's 3-argument form after `instrument()` | +| `--sdk v1\|v2` | client-side: which SDK major serves the URL (era-conditional assertions) | +| `--conv on` | client-side: expect the injected parameter and echo the handle | + +Same shape for a Nest harness: `LEVEL=low node harness/nest-v2/verify.mjs`. + +## Why it is built this way + +- **Real HTTP, not in-process transports.** Two assertions are impossible in process: _"the + response carries no `mcp-session-id` header"_ is an absence you can only observe on the wire, + and `createMcpHandler`'s per-request server instances only exist under a real handler. +- **Raw JSON-RPC over `fetch`, never the SDK `Client`.** A stock v2 `Client` negotiates the + **legacy** era and sends an `initialize` handshake — it exercises none of 2026-07-28 and + reports green anyway. +- **The verifiers replay `Mcp-Session-Id`**, because a real client does. On a stateless server + that replayed token is the only thing carrying client info and the negotiated protocol version + between per-request instances. +- **Identical tool surface everywhere** — `echo`, `add`, `fail_always`. If a number differs + between harnesses it is the SDK or the adapter doing it, not the fixture. +- **Ephemeral ports.** Fixed shared ports let a row occasionally reach the _previous_ row's dying + server, which shows up as a red cell indistinguishable from a regression. +- **Assertions, not eyeballs.** Every verifier exits non-zero; the matrix reconciles against its + expected-failures snapshot. +- **Match handles, not wording.** A fixture that pins the sentence carrying a value turns an + intended wire-format change into a red cell indistinguishable from a regression. Assert the + value. + +What the matrix cannot see: its fixture fails on the **last** call, so first-call error ordering +needs `probe-first-call-error.mjs`; every catalogue fits on one page, so envelope loss needs +`probe-pagination.mjs`. A column that is green because the fixture never reaches the code is +indistinguishable, in the grid, from one that is green because the code is right. + +One thing this gate cannot answer: whether a model **chooses** to cooperate (above all +`enableConversationId`, whose mechanism is the agent threading a handle back). That needs a +model-in-the-loop rig, which is not part of CI — ask the package maintainers before changing the +conversation-id delivery contract. + +## Wire formats the client lanes must send + +- **Legacy (`2025-11-25`)** requires `MCP-Protocol-Version` on every request after `initialize`. +- **Modern (`2026-07-28`)** needs **all** of: `params._meta` carrying + `io.modelcontextprotocol/protocolVersion`, `/clientInfo` **and** `/clientCapabilities` (omitting + capabilities returns `-32602 Invalid _meta envelope`); an `Mcp-Method` header matching the + body's method; an `Mcp-Name` header for `tools/call` / `prompts/get` / `resources/read`; and no + handshake. + +`NodeStreamableHTTPServerTransport` tops out at `2025-11-25`, so **only `createMcpHandler` serves +the modern era**, and it is per-request by construction. diff --git a/packages/mcp/harness/dual-era/client/run.mjs b/packages/mcp/harness/dual-era/client/run.mjs new file mode 100644 index 0000000000..eef82debda --- /dev/null +++ b/packages/mcp/harness/dual-era/client/run.mjs @@ -0,0 +1,226 @@ +// Raw JSON-RPC over fetch. Not the SDK Client — a stock v2 Client negotiates the +// LEGACY era and would report green having tested none of 2026-07-28. +// +// node client/run.mjs --url http://localhost: --sdk v2 --lane 2026 [--conv on|off] [--json] +// +// Emits the matrix assertions. --json prints one machine-readable line for +// matrix.mjs; without it, a human-readable list. + +const argv = process.argv.slice(2) +const arg = (name, fallback) => { + const i = argv.indexOf(`--${name}`) + return i === -1 ? fallback : argv[i + 1] +} +const URL_BASE = arg('url', 'http://localhost:3222') +// Which SDK major serves the URL. Some assertions are era/major-conditional and +// this cannot be sniffed off the wire (that ambiguity is the point of the tests). +const SDK = arg('sdk', 'v2') === 'v1' ? 'v1' : 'v2' +const LANE = arg('lane', '2025') === '2026' ? '2026' : '2025' +const CONV = arg('conv', 'off') === 'on' +const AS_JSON = argv.includes('--json') +// What this configuration requires of the mcp-session-id response header: +// none 2026-07-28 forbids minting one +// token v1 stateless — @posthog/mcp mints its own self-encoded token +// present v1 stateful — the transport mints an opaque id +const HEADER_EXPECT = arg('header', 'none') +const ECHO = process.env.ECHO !== '0' + +const MODERN = '2026-07-28' +const LEGACY = '2025-11-25' +const CLIENT_INFO = { name: 'dual-era-testbed', version: '1.0.0' } +const INTENT = 'checking adoption numbers' +const META = { + protocolVersion: 'io.modelcontextprotocol/protocolVersion', + clientInfo: 'io.modelcontextprotocol/clientInfo', + clientCapabilities: 'io.modelcontextprotocol/clientCapabilities', +} +/** Methods whose Mcp-Name header must mirror a params field (2026-07-28 wire rule). */ +const NAME_HEADER_SOURCE = { 'tools/call': 'name', 'prompts/get': 'name', 'resources/read': 'uri' } + +let id = 0 +let sessionHeader +const seenSessionHeaders = [] + +async function rpc(method, params = {}) { + const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' } + let body + + if (LANE === '2026') { + body = { + jsonrpc: '2.0', + id: ++id, + method, + params: { + ...params, + _meta: { + [META.protocolVersion]: MODERN, + [META.clientInfo]: CLIENT_INFO, + [META.clientCapabilities]: {}, + }, + }, + } + headers['mcp-protocol-version'] = MODERN + headers['mcp-method'] = method + const nameField = NAME_HEADER_SOURCE[method] + if (nameField && typeof params[nameField] === 'string') headers['mcp-name'] = params[nameField] + } else { + body = { jsonrpc: '2.0', id: ++id, method, params } + if (sessionHeader) headers['mcp-session-id'] = sessionHeader + // 2025-11-25 requires the negotiated version on every request after + // initialize. A real client sends it; without it this lane silently + // tested a client that does not exist. + if (method !== 'initialize') headers['mcp-protocol-version'] = LEGACY + } + + const res = await fetch(`${URL_BASE}/mcp`, { method: 'POST', headers, body: JSON.stringify(body) }) + const got = res.headers.get('mcp-session-id') + seenSessionHeaders.push({ method, header: got }) + if (got && !sessionHeader) sessionHeader = got + + const text = await res.text() + if (!text) return { status: res.status } + const payload = + text.startsWith('event:') || text.startsWith('data:') + ? JSON.parse( + text + .split('\n') + .find((l) => l.startsWith('data:')) + .slice(5) + .trim() + ) + : JSON.parse(text) + return { status: res.status, ...payload } +} + +async function notify(method, params = {}) { + const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' } + if (sessionHeader) headers['mcp-session-id'] = sessionHeader + await fetch(`${URL_BASE}/mcp`, { + method: 'POST', + headers, + body: JSON.stringify({ jsonrpc: '2.0', method, params }), + }) +} + +const textOf = (r) => (r?.result?.content ?? []).map((c) => c.text ?? '').join('\n') + +/** + * Pull the conversation handle out of a tool result. + * + * The handle arrives as data — a `{"conversation_id":"…"}` text block (#4542) — + * because an imperative sentence in tool output is indistinguishable from prompt + * injection to a hardened client. Match on the handle, never on the wording: a + * fixture that pins the sentence turns an intended wire-format change into a red + * cell indistinguishable from a regression. + */ +const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' +const HANDLE_PATTERN = new RegExp(`"conversation_id"\\s*:\\s*"(${UUID})"`, 'i') +const handleFrom = (r) => textOf(r).match(HANDLE_PATTERN)?.[1] + +async function scenario() { + await fetch(`${URL_BASE}/__reset`).catch(() => {}) + + if (LANE === '2025') { + await rpc('initialize', { protocolVersion: LEGACY, capabilities: {}, clientInfo: CLIENT_INFO }) + await notify('notifications/initialized') + } + + const list = await rpc('tools/list') + const first = await rpc('tools/call', { name: 'echo', arguments: { text: 'one' } }) + const handle = handleFrom(first) + const withHandle = (extra) => (ECHO && handle ? { ...extra, conversation_id: handle } : extra) + + // The second echo carries the injected `context` parameter — an agent stating + // its intent. The tool must still answer correctly with the extra argument on + // the wire, and $mcp_intent must be captured from it. + const second = await rpc('tools/call', { + name: 'echo', + arguments: withHandle({ text: 'two', context: INTENT }), + }) + await rpc('tools/call', { name: 'add', arguments: withHandle({ a: 2, b: 3 }) }) + await rpc('tools/call', { name: 'fail_always', arguments: withHandle({}) }) + + const state = await fetch(`${URL_BASE}/__events`).then((r) => r.json()) + return { list, second, handle, ...state } +} + +// ── the assertions ────────────────────────────────────────────────────────── +const r = await scenario() +const ev = r.events ?? [] +const of = (t) => ev.filter((e) => e.event === t) +const toolCalls = of('$mcp_tool_call') +const p = (e, k) => e?.properties?.[k] + +const errCall = toolCalls.find((e) => p(e, '$mcp_tool_name') === 'fail_always') +const callSessions = new Set(toolCalls.map((e) => p(e, '$session_id')).filter(Boolean)) +const echoTool = (r.list?.result?.tools ?? []).find((t) => t.name === 'echo') +const advertises = Object.keys(echoTool?.inputSchema?.properties ?? {}).includes('conversation_id') +const isV2 = SDK === 'v2' + +function decodesAsOurToken(value) { + if (!value) return false + try { + const payload = JSON.parse(Buffer.from(value, 'base64url').toString('utf8')) + return typeof payload?.sid === 'string' && payload.sid.startsWith('ses_') + } catch { + return false + } +} +// 2026-07-28 MUST NOT: no session header may be minted on a 2026-era exchange. +// The MUST NOT is era-scoped, not SDK-scoped — a v2 server serving the 2025 era +// legitimately mints one (ADR-0009), which the longlived rows assert via +// --header token. Make the invariant structural instead of contingent on which +// rows exist: fail fast if a fixture ever expects a header on the modern era. +if (LANE === '2026' && HEADER_EXPECT !== 'none') { + console.error( + `fatal: --lane 2026 requires --header none (2026-07-28 forbids minting a session header), got '${HEADER_EXPECT}'` + ) + process.exit(1) +} + +const mintedHeaders = seenSessionHeaders.map((s) => s.header).filter(Boolean) +const headerOk = + HEADER_EXPECT === 'none' + ? mintedHeaders.length === 0 + : HEADER_EXPECT === 'token' + ? mintedHeaders.length > 0 && decodesAsOurToken(mintedHeaders[0]) + : mintedHeaders.length > 0 + +const results = { + calls: + toolCalls.length === 4 && toolCalls.map((e) => p(e, '$mcp_tool_name')).join(',') === 'echo,echo,add,fail_always', + // Subsumes the old 'message' column: an error is only captured correctly if its + // message is clean — no conversation_id prompt-back concatenated into it. + errors: + p(errCall, '$mcp_is_error') === true && + of('$exception').length >= 1 && + p(errCall, '$mcp_error_message') === 'intentional failure', + // The agent stated its intent via the injected `context` parameter: it must be + // captured as $mcp_intent, and the tool must still have answered correctly with + // the extra argument on the wire. + intent: toolCalls.some((e) => p(e, '$mcp_intent') === INTENT) && textOf(r.second) === 'two', + // The flag must visibly take effect: advertised iff enabled. + schema: advertises === CONV, + session: isV2 && !CONV ? null : toolCalls.length > 0 && callSessions.size === 1, + client: + toolCalls.length > 0 && + toolCalls.every( + (e) => p(e, '$mcp_client_name') === CLIENT_INFO.name && p(e, '$mcp_client_version') === CLIENT_INFO.version + ), + protocol: + toolCalls.length > 0 && + toolCalls.every((e) => p(e, '$mcp_protocol_version') === (LANE === '2026' ? MODERN : LEGACY)), + warnings: (r.warnings ?? []).length === 0, + header: headerOk, + alive: null, // measured by matrix.mjs in a separate boot +} + +if (AS_JSON) { + console.log(JSON.stringify({ results, events: ev.length, warning: (r.warnings ?? [])[0] ?? null })) +} else { + for (const [k, v] of Object.entries(results)) { + console.log(` ${v === null ? ' · ' : v ? '\x1b[32m ✓ \x1b[0m' : '\x1b[31m ✗ \x1b[0m'} ${k}`) + } + if ((r.warnings ?? [])[0]) console.log(`\n warning: ${r.warnings[0]}`) +} +process.exit(Object.values(results).some((v) => v === false) ? 1 : 0) diff --git a/packages/mcp/harness/dual-era/expected-failures.json b/packages/mcp/harness/dual-era/expected-failures.json new file mode 100644 index 0000000000..37c69d9ba7 --- /dev/null +++ b/packages/mcp/harness/dual-era/expected-failures.json @@ -0,0 +1,32 @@ +[ + { + "row": "v2 high 2025 conv=off", + "col": "client", + "why": "Documented limitation: on createMcpHandler's legacy (2025-era) leg, clientInfo exists only at a handshake a per-request instance never saw, and the session token that would carry it cannot reach the wire" + }, + { + "row": "v2 high 2025 conv=on", + "col": "client", + "why": "Same legacy-leg clientInfo limitation" + }, + { + "row": "v2 low 2025 conv=off", + "col": "client", + "why": "Same legacy-leg clientInfo limitation" + }, + { + "row": "v2 low 2025 conv=on", + "col": "client", + "why": "Same legacy-leg clientInfo limitation" + }, + { + "row": "v2 low 2025 conv=on", + "col": "session", + "why": "C2, parked: the low-level path has no tool registry to read conversation_id ownership from, so a cold per-request instance never correlates the session" + }, + { + "row": "v2 low 2026 conv=on", + "col": "session", + "why": "Same parked C2 low-level ownership gap" + } +] diff --git a/packages/mcp/harness/dual-era/matrix.mjs b/packages/mcp/harness/dual-era/matrix.mjs new file mode 100644 index 0000000000..1f6e12572e --- /dev/null +++ b/packages/mcp/harness/dual-era/matrix.mjs @@ -0,0 +1,340 @@ +// Boots every configuration, runs the assertions, renders the matrix. +// +// node matrix.mjs all 12 rows +// node matrix.mjs --major v1 the 4 SDK-v1 rows +// node matrix.mjs --major v2 the 8 SDK-v2 rows +// +// This is the CI gate, not just a report: it exits non-zero when a row produces +// no verdict (server never booted, client died mid-run) or when the set of red +// cells differs from expected-failures.json in EITHER direction — a regression +// fails, and so does an unexpected improvement (remove the entry to ratchet it +// in). Servers bind ephemeral ports (PORT=0) and announce the chosen port on +// stdout, so rows cannot collide or reach a dying neighbour. +import { spawn } from 'node:child_process' +import { setTimeout as sleep } from 'node:timers/promises' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +const DIR = fileURLToPath(new URL('.', import.meta.url)) + +/** The build under test and the SDK majors it runs against — resolved, not assumed. */ +function versionBanner() { + // fs reads, not require(): both SDKs' `exports` deny the package.json subpath. + const version = (rel) => { + try { + return JSON.parse(readFileSync(new URL(rel, import.meta.url), 'utf8')).version + } catch { + return '?' + } + } + const local = version('../../package.json') + const v1 = version('../../node_modules/@modelcontextprotocol/sdk/package.json') + const v2 = version('../../node_modules/@modelcontextprotocol/server/package.json') + return `@posthog/mcp ${local} (workspace build) · sdk v1 ${v1} · v2 ${v2}` +} + +const MAJOR = (() => { + const i = process.argv.indexOf('--major') + return i === -1 ? 'all' : process.argv[i + 1] +})() + +const COLUMNS = [ + ['calls', 5], + ['errors', 6], + ['intent', 6], + ['schema', 6], + ['session', 7], + ['client', 6], + ['protocol', 8], + ['warnings', 8], + ['header', 6], + ['alive', 5], +] + +// label, server file, env, lane, conv, header expectation +const V1_ROWS = [ + ['v1 stateful high', 'v1.mjs', { LEVEL: 'high', MODE: 'stateful' }, '2025', 'off', 'present'], + ['v1 stateful low', 'v1.mjs', { LEVEL: 'low', MODE: 'stateful' }, '2025', 'off', 'present'], + ['v1 stateless high', 'v1.mjs', { LEVEL: 'high', MODE: 'stateless' }, '2025', 'off', 'token'], + ['v1 stateless low', 'v1.mjs', { LEVEL: 'low', MODE: 'stateless' }, '2025', 'off', 'token'], +] +// The first eight rows run MODE=perrequest (the default): createMcpHandler +// builds a fresh server per HTTP request, the only modern-capable topology. +// The two longlived rows are the control — one server for the whole process on +// NodeStreamableHTTPServerTransport. That transport tops out at 2025-11-25, so +// there is no longlived 2026 row, and on the legacy era the server mints an +// Mcp-Session-Id (ADR-0009), so these rows expect our token where the +// per-request rows expect no header at all. +const V2_ROWS = [ + ['v2 high 2025 conv=off', 'v2.mjs', { LEVEL: 'high' }, '2025', 'off'], + ['v2 high 2025 conv=on', 'v2.mjs', { LEVEL: 'high' }, '2025', 'on'], + ['v2 high 2026 conv=off', 'v2.mjs', { LEVEL: 'high' }, '2026', 'off'], + ['v2 high 2026 conv=on', 'v2.mjs', { LEVEL: 'high' }, '2026', 'on'], + ['v2 low 2025 conv=off', 'v2.mjs', { LEVEL: 'low' }, '2025', 'off'], + ['v2 low 2025 conv=on', 'v2.mjs', { LEVEL: 'low' }, '2025', 'on'], + ['v2 low 2026 conv=off', 'v2.mjs', { LEVEL: 'low' }, '2026', 'off'], + ['v2 low 2026 conv=on', 'v2.mjs', { LEVEL: 'low' }, '2026', 'on'], + ['v2 high longlived 2025', 'v2.mjs', { LEVEL: 'high', MODE: 'longlived' }, '2025', 'off', 'token'], + ['v2 low longlived 2025', 'v2.mjs', { LEVEL: 'low', MODE: 'longlived' }, '2025', 'off', 'token'], +] +const ROWS = MAJOR === 'v1' ? V1_ROWS : MAJOR === 'v2' ? V2_ROWS : [...V1_ROWS, null, ...V2_ROWS] + +const children = new Set() + +/** + * Boot a server on an ephemeral port and resolve with { child, port } once it + * announces `MCP_HARNESS_LISTENING port=` on stdout, or { child, port: null } + * if it never does. + */ +function boot(file, env) { + return new Promise((resolve) => { + const child = spawn('node', [`${DIR}servers/${file}`], { + env: { ...process.env, ...env, PORT: '0' }, + stdio: ['ignore', 'pipe', 'inherit'], + }) + children.add(child) + let out = '' + let settled = false + const settle = (port) => { + if (settled) return + settled = true + resolve({ child, port }) + } + child.stdout.on('data', (d) => { + out += d + const m = out.match(/MCP_HARNESS_LISTENING port=(\d+)/) + if (m) settle(Number(m[1])) + }) + child.on('exit', () => settle(null)) + setTimeout(() => settle(null), 15000) + }) +} +async function stop(child) { + child.kill('SIGKILL') + children.delete(child) + await sleep(100) +} +async function waitUp(port) { + for (let i = 0; i < 80; i++) { + try { + await fetch(`http://localhost:${port}/__events`) + return true + } catch { + await sleep(150) + } + } + return false +} + +const COLUMN_NAMES = COLUMNS.map(([n]) => n) +const lastLine = (s) => s.trim().split('\n').filter(Boolean).pop() ?? '' + +/** The one line of a Node crash dump that names the failure, for the summary. */ +function whyItDied(stderr) { + const lines = stderr + .split('\n') + .map((l) => l.trim()) + .filter(Boolean) + // A crash dump ends with `Node.js v22.x`, so the last line is never the reason. + return lines.find((l) => /^[\w$.]*(?:Error|Exception)\b/.test(l)) ?? lines[0] ?? '' +} + +/** + * Run the client for one row and resolve with { results } or { error, detail }. + * + * A client that dies before reporting must never resolve to a partial verdict: + * an absent assertion renders as `·` (not applicable) and is skipped by the + * reconciliation, so a crash would read as a green row. Anything short of a + * verdict for every column is an error, and an error fails the lane. + * + * A non-zero exit is NOT an error — the client exits 1 whenever an assertion is + * red, which is a result, not a crash. + */ +function runClient(port, sdk, lane, conv, headerExpect) { + return new Promise((resolve) => { + const child = spawn( + 'node', + [ + `${DIR}client/run.mjs`, + ...['--url', `http://localhost:${port}`, '--sdk', sdk, '--lane', lane], + ...['--conv', conv, '--header', headerExpect, '--json'], + ], + { env: process.env, stdio: ['ignore', 'pipe', 'pipe'] } + ) + let out = '' + let err = '' + // Both pipes must be drained: an unread stderr can wedge the child once + // the pipe buffer fills, and its tail is the only clue when one crashes. + child.stdout.on('data', (d) => (out += d)) + child.stderr.on('data', (d) => (err += d)) + child.on('error', (e) => resolve({ error: `client could not be spawned: ${e.message}` })) + child.on('close', (code) => { + let results + try { + results = JSON.parse(lastLine(out)).results + } catch { + const why = whyItDied(err) || lastLine(out) || 'no output' + resolve({ error: `client exited ${code} without a verdict — ${why}`, detail: err }) + return + } + const missing = COLUMN_NAMES.filter((n) => !(n in Object(results))) + if (missing.length > 0) { + resolve({ error: `client exited ${code} reporting nothing for: ${missing.join(', ')}`, detail: err }) + return + } + resolve({ results }) + }) + }) +} + +/** + * Separate boot: does the host server survive a 3-arg custom registration? + * The probe request speaks the row's own era — a longlived transport tops out + * at 2025-11-25, so a modern-era probe would report it dead when it is fine. + */ +async function checkAlive(file, env, lane) { + if (file === 'v1.mjs') return null // v1's setRequestHandler has no 3-argument form + const { child, port } = await boot(file, { ...env, CUSTOM_3ARG: '1' }) + if (!port || !(await waitUp(port))) { + await stop(child) + return false + } + const modern = lane === '2026' + let ok = false + try { + const res = await fetch(`http://localhost:${port}/mcp`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + 'mcp-protocol-version': modern ? '2026-07-28' : '2025-11-25', + ...(modern ? { 'mcp-method': 'tools/list' } : {}), + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'tools/list', + params: modern + ? { + _meta: { + 'io.modelcontextprotocol/protocolVersion': '2026-07-28', + 'io.modelcontextprotocol/clientInfo': { name: 'p', version: '1' }, + 'io.modelcontextprotocol/clientCapabilities': {}, + }, + } + : {}, + }), + }) + ok = res.status === 200 + } catch { + ok = false + } + await stop(child) + return ok +} + +// ── run ───────────────────────────────────────────────────────────────────── +const rendered = [] +for (const row of ROWS) { + if (row === null) { + rendered.push(null) + continue + } + const [label, file, env, lane, conv, headerExpect = 'none'] = row + const sdk = file === 'v1.mjs' ? 'v1' : 'v2' + const { child, port } = await boot(file, { ...env, CONVERSATION_ID: conv === 'on' ? '1' : '0' }) + if (!port || !(await waitUp(port))) { + await stop(child) + rendered.push([label, null, 'server did not start']) + continue + } + const { results, error, detail } = await runClient(port, sdk, lane, conv, headerExpect) + await stop(child) + if (error) { + rendered.push([label, null, error]) + process.stderr.write(` ran ${label} — ${error}\n`) + // The crash dump, indented: the summary line alone rarely says enough to fix + // it from a CI log. Capped — a client that spews should not bury the matrix. + const dump = (detail ?? '').trimEnd().split('\n').slice(0, 40) + if (dump.join('').trim()) process.stderr.write(dump.join('\n').replace(/^/gm, ' ') + '\n') + continue + } + results.alive = await checkAlive(file, env, lane) + rendered.push([label, results]) + process.stderr.write(` ran ${label}\n`) +} + +// ── render ────────────────────────────────────────────────────────────────── +const LABEL_W = 30 +const cell = (v) => (v === null || v === undefined ? '·' : v ? '✓' : '✗') +const centre = (s, w) => { + const pad = w - s.length + return ' '.repeat(Math.floor(pad / 2)) + s + ' '.repeat(Math.ceil(pad / 2)) +} + +const groups = [ + ['capture', 4], + ['identity', 3], + ['safety', 3], +] +let g1 = ' '.repeat(LABEL_W) +let idx = 0 +for (const [name, span] of groups) { + const w = COLUMNS.slice(idx, idx + span).reduce((a, [, cw]) => a + cw + 1, 0) + const inner = ` ${name} ` + const dashes = Math.max(0, w - inner.length) + g1 += '─'.repeat(Math.floor(dashes / 2)) + inner + '─'.repeat(Math.ceil(dashes / 2)) + idx += span +} +const head = ' '.repeat(LABEL_W) + COLUMNS.map(([n, w]) => centre(n, w + 1)).join('') +const rule = '─'.repeat(head.length) + +console.log(`\n${g1}\n${head}\n${rule}`) +for (const row of rendered) { + if (row === null) { + console.log(rule) + continue + } + const [label, results, reason] = row + if (!results) { + console.log(label.padEnd(LABEL_W) + ` ${reason}`) + continue + } + console.log(label.padEnd(LABEL_W) + COLUMNS.map(([n, w]) => centre(cell(results[n]), w + 1)).join('')) +} +console.log(rule) +console.log(` ✓ pass ✗ fail · not applicable ${versionBanner()}`) + +for (const c of children) c.kill('SIGKILL') + +// ── expected-failures reconciliation ──────────────────────────────────────── +const norm = (s) => s.replace(/\s+/g, ' ').trim() +const expected = JSON.parse(readFileSync(new URL('./expected-failures.json', import.meta.url), 'utf8')) + .filter((f) => (MAJOR === 'all' ? true : norm(f.row).startsWith(MAJOR))) + .map((f) => ({ row: norm(f.row), key: `${norm(f.row)} · ${f.col}` })) +const expectedKeys = new Set(expected.map((e) => e.key)) +const failing = new Set() +// Rows that produced no verdict at all. They are a hard failure in their own +// right — never a row of `·` cells that the reconciliation would wave through. +const unreported = [] +const unreportedRows = new Set() +for (const row of rendered) { + if (row === null) continue + const [label, results, reason] = row + if (!results) { + unreported.push(`${norm(label)} — ${reason}`) + unreportedRows.add(norm(label)) + continue + } + for (const [n] of COLUMNS) if (results[n] === false) failing.add(`${norm(label)} · ${n}`) +} +const regressed = [...failing].filter((k) => !expectedKeys.has(k)) +// An unreported row proves nothing about its expected failures, so don't claim +// they started passing — that would bury the real error under a stale-snapshot one. +const nowPassing = expected.filter((e) => !failing.has(e.key) && !unreportedRows.has(e.row)).map((e) => e.key) + +if (unreported.length > 0) console.error(`\nno verdict: ${unreported.join(' · ')}`) +if (regressed.length > 0) console.error(`\nregressed: ${regressed.join(' · ')}`) +if (nowPassing.length > 0) + console.error(`\nnow passing — remove from expected-failures.json: ${nowPassing.join(' · ')}`) +process.exit(unreported.length === 0 && regressed.length === 0 && nowPassing.length === 0 ? 0 : 1) diff --git a/packages/mcp/harness/dual-era/probe-first-call-error.mjs b/packages/mcp/harness/dual-era/probe-first-call-error.mjs new file mode 100644 index 0000000000..81b4305dba --- /dev/null +++ b/packages/mcp/harness/dual-era/probe-first-call-error.mjs @@ -0,0 +1,140 @@ +// C1 probe — the case the matrix does not cover. +// +// The matrix drives four tool calls and fails on the LAST one, echoing the +// conversation handle it received on the first. So `minted` is false by the time +// `fail_always` runs, no prompt-back is appended to that result, and there is +// nothing to pollute the error message with — the matrix's `errors` column +// asserts a clean message and passes without ever exercising the bug. +// +// The pollution needs the FIRST call of a conversation to fail: that is the call +// that mints the handle, appends it to the result, and — before the fix — read +// the captured error back out of the appended copy, splicing a fresh uuid into +// $mcp_error_message on every failure. (Since 0.11.7 the handle is appended as +// `{"conversation_id":"…"}` rather than an imperative sentence; the ordering bug +// this probe covers is independent of which form it takes.) +// +// A tool failing on the first call of a conversation is not a corner case: it is +// what an agent hits on a bad argument, an expired token, or a cold dependency. +// +// v2 only, and not for convenience: on v1 the thrown error is stashed by the +// tool-callback wrapper and captured from there, so the appended result is never +// read and the bug cannot occur. v2 flattens the throw into an `isError` result +// before our wrapper sees it, which is what makes that result the only +// description of the failure — and therefore what exposes the pollution. +// +// node probe-first-call-error.mjs +import { createServer } from 'node:http' +import { setTimeout as sleep } from 'node:timers/promises' +import { McpServer as V2McpServer, createMcpHandler } from '@modelcontextprotocol/server' +import { toNodeHandler } from '@modelcontextprotocol/node' +import { instrument } from '@posthog/mcp' +import { TOOLS, TOOL_BY_NAME } from './shared/tools.mjs' +import { createRecorder } from './shared/posthog.mjs' + +const GREEN = '\x1b[32m' +const RED = '\x1b[31m' +const RESET = '\x1b[0m' +const results = [] +const check = (name, ok, detail = '') => { + results.push({ name, ok }) + console.log(` ${ok ? GREEN + '✓' : RED + '✗'}${RESET} ${name}${detail ? ` ${detail}` : ''}`) +} + +/** + * The high-level server on both majors. Ownership of `conversation_id` is read + * from the tool registry there, which is what lets a handle be minted at all — + * on a low-level server it is learned from a `tools/list` this instance served, + * so a cold per-request instance never mints one and the bug is unreachable. + */ +function registerAll(server) { + for (const t of TOOLS) { + server.registerTool(t.name, { description: t.description, inputSchema: t.inputShape }, t.handler) + } + return server +} + +const MODERN_META = { + 'io.modelcontextprotocol/protocolVersion': '2026-07-28', + 'io.modelcontextprotocol/clientInfo': { name: 'probe', version: '1.0.0' }, + 'io.modelcontextprotocol/clientCapabilities': {}, +} + +async function post(port, body, headers = {}) { + const res = await fetch(`http://localhost:${port}/mcp`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + ...headers, + }, + body: JSON.stringify(body), + }) + return { status: res.status, text: await res.text() } +} + +/** + * Does the response carry a conversation handle? + * + * The handle is returned as data — `{"conversation_id":"…"}` (#4542) — rather + * than as an imperative sentence a hardened client reads as prompt injection. + * Assert the handle reached the caller, not the sentence carrying it. + */ +const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' +const HANDLE = new RegExp(`conversation_id"\\s*:\\s*"${UUID}`, 'i') + +/** `responseText` is a raw HTTP body, so the handle's own JSON arrives escaped. */ +const deliversHandle = (responseText) => HANDLE.test(responseText.replace(/\\"/g, '"')) + +/** Both assertions matter, and they pull in opposite directions. */ +function assertBoth(label, recorder, responseText) { + const call = recorder.events.filter((e) => e.event === '$mcp_tool_call').at(-1) + const message = call?.properties?.$mcp_error_message + check(`${label} · error captured`, call?.properties?.$mcp_is_error === true) + check(`${label} · $mcp_error_message is clean`, message === 'intentional failure', JSON.stringify(message)) + // The agent must still receive the handle on a failed call — otherwise the + // retry opens a new conversation and the failure and its fix land in + // different sessions. Fixing the message by withholding it would be worse. + check(`${label} · handle still delivered to the caller`, deliversHandle(responseText)) +} + +// ── v2, high-level, modern era, conversation ids on ───────────────────────── +async function probeV2() { + console.log('\nv2 · first call of the conversation fails · modern era · conv=on') + const recorder = createRecorder('probe:v2') + const handler = createMcpHandler( + () => { + const server = registerAll(new V2McpServer({ name: 'probe-v2', version: '1.0.0' })) + instrument(server, recorder.client, { logger: recorder.logger, enableConversationId: true }) + return server + }, + { responseMode: 'json', onerror: (e) => recorder.logger(`handler error: ${e}`) } + ) + const node = toNodeHandler(handler) + const http = createServer((req, res) => node(req, res)).listen(0) + await sleep(300) + const port = http.address().port + try { + // No tools/list first, and no handle supplied: this call mints one. + const call = await post( + port, + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'fail_always', arguments: {}, _meta: MODERN_META }, + }, + { 'mcp-protocol-version': '2026-07-28', 'mcp-method': 'tools/call', 'mcp-name': 'fail_always' } + ) + await sleep(200) + assertBoth('v2', recorder, call.text) + } finally { + http.close() + await sleep(150) + } +} + +await probeV2() + +const passed = results.filter((r) => r.ok).length +console.log(`\n${passed}/${results.length} passed`) +process.exit(passed === results.length ? 0 : 1) diff --git a/packages/mcp/harness/dual-era/probe-late-handlers.mjs b/packages/mcp/harness/dual-era/probe-late-handlers.mjs new file mode 100644 index 0000000000..098cb552c4 --- /dev/null +++ b/packages/mcp/harness/dual-era/probe-late-handlers.mjs @@ -0,0 +1,196 @@ +// Late-registration probe — the case the matrix does not cover. +// +// Both servers in the matrix register `tools/list` / `tools/call` BEFORE +// instrument(), so the synthetic `tools/call` fallback path never runs there. +// This probe registers them AFTER — the mcp-nest / adapter shape — on both SDK +// majors, and asserts instrument() stayed quiet and the late-registered +// dispatcher was still instrumented. +// +// node probe-late-handlers.mjs both majors +// node probe-late-handlers.mjs --major v1|v2 one major (what each CI lane runs, +// so a red probe names the stack) +import { createServer } from 'node:http' +import { randomUUID } from 'node:crypto' +import { setTimeout as sleep } from 'node:timers/promises' +import { Server as V1Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { + CallToolRequestSchema as V1CallToolRequestSchema, + ListToolsRequestSchema as V1ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js' +import { Server as V2Server, createMcpHandler } from '@modelcontextprotocol/server' +import { toNodeHandler } from '@modelcontextprotocol/node' +import { instrument } from '@posthog/mcp' +import { TOOLS, TOOL_BY_NAME } from './shared/tools.mjs' +import { createRecorder } from './shared/posthog.mjs' + +const GREEN = '\x1b[32m' +const RED = '\x1b[31m' +const RESET = '\x1b[0m' +const results = [] +const check = (name, ok, detail = '') => { + results.push({ name, ok }) + console.log(` ${ok ? GREEN + '✓' : RED + '✗'}${RESET} ${name}${detail ? ` ${detail}` : ''}`) +} + +const listHandler = async () => ({ + tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.jsonSchema })), +}) +const callHandler = async (request) => { + const tool = TOOL_BY_NAME[request.params.name] + if (!tool) throw new Error(`Unknown tool: ${request.params.name}`) + return tool.handler(request.params.arguments ?? {}) +} + +const MODERN_META = { + 'io.modelcontextprotocol/protocolVersion': '2026-07-28', + 'io.modelcontextprotocol/clientInfo': { name: 'probe', version: '1.0.0' }, + 'io.modelcontextprotocol/clientCapabilities': {}, +} + +async function post(port, body, headers = {}) { + const res = await fetch(`http://localhost:${port}/mcp`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + ...headers, + }, + body: JSON.stringify(body), + }) + const text = await res.text() + return { status: res.status, text, sessionId: res.headers.get('mcp-session-id') } +} + +// ── v2, low-level, handlers registered after instrument() ─────────────────── +async function probeV2() { + console.log('\nv2 low-level · handlers registered AFTER instrument() · modern era') + const recorder = createRecorder('probe:v2') + const handler = createMcpHandler( + () => { + const server = new V2Server({ name: 'probe-v2', version: '1.0.0' }, { capabilities: { tools: {} } }) + instrument(server, recorder.client, { logger: recorder.logger }) + server.setRequestHandler('tools/list', listHandler) + server.setRequestHandler('tools/call', callHandler) + return server + }, + { responseMode: 'json', onerror: (e) => recorder.logger(`handler error: ${e}`) } + ) + const node = toNodeHandler(handler) + const http = createServer((req, res) => node(req, res)).listen(0) + await sleep(300) + const port = http.address().port + try { + const call = await post( + port, + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'echo', arguments: { text: 'hi' }, _meta: MODERN_META }, + }, + { 'mcp-protocol-version': '2026-07-28', 'mcp-method': 'tools/call', 'mcp-name': 'echo' } + ) + await sleep(200) + + check('instrument() logged no warning', recorder.warnings.length === 0, recorder.warnings.join(' | ')) + check('tool call answered', call.status === 200 && call.text.includes('hi'), `HTTP ${call.status}`) + const calls = recorder.events.filter((e) => e.event === '$mcp_tool_call') + check('$mcp_tool_call captured', calls.length === 1, `got ${calls.length}`) + check( + 'tool name recorded', + calls[0]?.properties?.$mcp_tool_name === 'echo', + String(calls[0]?.properties?.$mcp_tool_name) + ) + } finally { + http.close() + await sleep(150) + } +} + +// ── v1, low-level, no tools capability declared ───────────────────────────── +// PR 1 stops routing the fallback through setRequestHandler, which asserted the +// capability. This is the behaviour change it carries; it must not break v1. +async function probeV1NoCapability() { + console.log('\nv1 low-level · server declares NO tools capability') + const recorder = createRecorder('probe:v1-nocap') + const server = new V1Server({ name: 'probe-v1', version: '1.0.0' }, { capabilities: {} }) + instrument(server, recorder.client, { logger: recorder.logger }) + check('instrument() logged no warning', recorder.warnings.length === 0, recorder.warnings.join(' | ')) + check('tools/call fallback registered', server._requestHandlers.has('tools/call')) +} + +// ── v1, low-level, handlers registered after instrument() ─────────────────── +async function probeV1Late() { + console.log('\nv1 low-level · handlers registered AFTER instrument() · legacy era') + const recorder = createRecorder('probe:v1-late') + const server = new V1Server({ name: 'probe-v1-late', version: '1.0.0' }, { capabilities: { tools: {} } }) + instrument(server, recorder.client, { logger: recorder.logger }) + server.setRequestHandler(V1ListToolsRequestSchema, listHandler) + server.setRequestHandler(V1CallToolRequestSchema, callHandler) + + // Stateful, like servers/v1.mjs: the transport mints Mcp-Session-Id and the + // client replays it. Sessions are v1's model; this probe is about handler + // registration order, so it uses the transport the way v1 intends. + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + enableJsonResponse: true, + }) + await server.connect(transport) + const http = createServer((req, res) => transport.handleRequest(req, res)).listen(0) + await sleep(300) + const port = http.address().port + try { + const init = await post(port, { + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2025-11-25', + capabilities: {}, + clientInfo: { name: 'probe', version: '1.0.0' }, + }, + }) + const session = init.sessionId ? { 'mcp-session-id': init.sessionId } : {} + const call = await post( + port, + { + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { name: 'echo', arguments: { text: 'hi' } }, + }, + session + ) + await sleep(200) + + check('instrument() logged no warning', recorder.warnings.length === 0, recorder.warnings.join(' | ')) + check( + 'tool call answered', + call.status === 200 && call.text.includes('hi'), + `HTTP ${call.status} ${call.text.slice(0, 200)}` + ) + const calls = recorder.events.filter((e) => e.event === '$mcp_tool_call') + check('$mcp_tool_call captured', calls.length === 1, `got ${calls.length}`) + } finally { + http.close() + await sleep(150) + } +} + +const MAJOR = (() => { + const i = process.argv.indexOf('--major') + return i === -1 ? 'all' : process.argv[i + 1] +})() + +if (MAJOR !== 'v2') { + await probeV1NoCapability() + await probeV1Late() +} +if (MAJOR !== 'v1') { + await probeV2() +} + +const passed = results.filter((r) => r.ok).length +console.log(`\n${passed}/${results.length} passed`) +process.exit(passed === results.length ? 0 : 1) diff --git a/packages/mcp/harness/dual-era/probe-pagination.mjs b/packages/mcp/harness/dual-era/probe-pagination.mjs new file mode 100644 index 0000000000..5b4a5af3e2 --- /dev/null +++ b/packages/mcp/harness/dual-era/probe-pagination.mjs @@ -0,0 +1,206 @@ +// tools/list envelope probe — the case no fixture in this harness covered. +// +// MCP paginates tools/list. A server whose catalogue spans pages returns page one +// plus a `nextCursor`, and the client enumerates until the cursor is absent. Our +// listing wrapper injects the analytics parameters into the advertised schemas and +// — before the fix — returned a freshly built `{ tools }`, dropping every other +// field the application's handler had put on the response. +// +// So the client saw no cursor, stopped after page one, and every tool on a later +// page became uncallable the moment instrument() was applied. No error on either +// side: the client believes it has the whole catalogue. +// +// This is the SDK removing behaviour the customer's server produced, which is why +// it needs a fixture of its own — the matrix asserts on captured events, and a +// catalogue that fits on one page can never show the loss. The same reason applies +// to `ttlMs` / `cacheScope` (the caching SEP-2549 added on 2026-07-28) and to +// result `_meta`: all three ride the same envelope. +// +// Both majors, because the defect is in our wrapper and not in either SDK. +// +// node probe-pagination.mjs +import { createServer } from 'node:http' +import { setTimeout as sleep } from 'node:timers/promises' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' +import { Server as V1Server } from '@modelcontextprotocol/sdk/server/index.js' +import { + CallToolRequestSchema, + CallToolResultSchema, + ListToolsRequestSchema, + ListToolsResultSchema, +} from '@modelcontextprotocol/sdk/types.js' +import { Server as V2Server, createMcpHandler } from '@modelcontextprotocol/server' +import { toNodeHandler } from '@modelcontextprotocol/node' +import { instrument } from '@posthog/mcp' +import { createRecorder } from './shared/posthog.mjs' + +const GREEN = '\x1b[32m' +const RED = '\x1b[31m' +const RESET = '\x1b[0m' +const results = [] +const check = (name, ok, detail = '') => { + results.push({ name, ok }) + console.log(` ${ok ? GREEN + '✓' : RED + '✗'}${RESET} ${name}${detail ? ` ${detail}` : ''}`) +} + +const PAGE_ONE = [{ name: 'page_one_tool', description: 'On page one', inputSchema: { type: 'object' } }] +const PAGE_TWO = [{ name: 'page_two_tool', description: 'On page two', inputSchema: { type: 'object' } }] + +/** Page one advertises the cursor and the 2026 cache directives; page two ends the enumeration. */ +const listPage = (cursor) => + cursor === 'page-2' + ? { tools: PAGE_TWO } + : { + tools: PAGE_ONE, + nextCursor: 'page-2', + ttlMs: 60_000, + cacheScope: 'public', + _meta: { 'com.posthog/probe': 'kept' }, + } + +const callResult = (name) => ({ content: [{ type: 'text', text: `called: ${name}` }] }) + +/** The four assertions, identical on both majors — only the transport differs. */ +function assertEnumeration(label, firstPage, secondPage, callText) { + check(`${label} · page one keeps nextCursor`, firstPage?.nextCursor === 'page-2', String(firstPage?.nextCursor)) + check( + `${label} · page two is reachable`, + secondPage?.tools?.length === 1 && secondPage.tools[0].name === 'page_two_tool', + JSON.stringify(secondPage?.tools?.map((t) => t.name)) + ) + // Enumeration is only worth anything if the tools it reaches are callable. + check(`${label} · a page-two tool can be called`, /called: page_two_tool/.test(callText ?? '')) + check( + `${label} · cache directives and result _meta survive`, + firstPage?.ttlMs === 60_000 && + firstPage?.cacheScope === 'public' && + firstPage?._meta?.['com.posthog/probe'] === 'kept', + JSON.stringify({ ttlMs: firstPage?.ttlMs, cacheScope: firstPage?.cacheScope, _meta: firstPage?._meta }) + ) +} + +// ── v1, low-level, in-memory ──────────────────────────────────────────────── +async function probeV1() { + console.log('\nv1 · low-level · paginated catalogue') + const recorder = createRecorder('probe:pagination:v1') + const server = new V1Server({ name: 'probe-v1', version: '1.0.0' }, { capabilities: { tools: {} } }) + server.setRequestHandler(ListToolsRequestSchema, async (request) => listPage(request.params?.cursor)) + server.setRequestHandler(CallToolRequestSchema, async (request) => callResult(request.params.name)) + instrument(server, recorder.client, { logger: recorder.logger }) + + const client = new Client({ name: 'probe', version: '1.0.0' }, { capabilities: {} }) + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair() + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]) + try { + const firstPage = await client.request({ method: 'tools/list', params: {} }, ListToolsResultSchema) + const secondPage = await client.request( + { method: 'tools/list', params: { cursor: firstPage.nextCursor } }, + ListToolsResultSchema + ) + let callText = '' + try { + const result = await client.request( + { method: 'tools/call', params: { name: 'page_two_tool', arguments: {} } }, + CallToolResultSchema + ) + callText = JSON.stringify(result) + } catch (error) { + callText = `error: ${error}` + } + assertEnumeration('v1', firstPage, secondPage, callText) + } finally { + await clientTransport.close?.() + await serverTransport.close?.() + } +} + +// ── v2, low-level, per-request factory, modern era, raw JSON-RPC ──────────── +const MODERN_META = { + 'io.modelcontextprotocol/protocolVersion': '2026-07-28', + 'io.modelcontextprotocol/clientInfo': { name: 'probe', version: '1.0.0' }, + 'io.modelcontextprotocol/clientCapabilities': {}, +} + +async function post(port, body, headers = {}) { + const res = await fetch(`http://localhost:${port}/mcp`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + 'mcp-protocol-version': '2026-07-28', + ...headers, + }, + body: JSON.stringify(body), + }) + const text = await res.text() + return { text, result: JSON.parse(text)?.result } +} + +async function probeV2() { + console.log('\nv2 · low-level · per-request · modern era · paginated catalogue') + const recorder = createRecorder('probe:pagination:v2') + const handler = createMcpHandler( + () => { + const server = new V2Server({ name: 'probe-v2', version: '1.0.0' }, { capabilities: { tools: {} } }) + server.setRequestHandler('tools/list', async (request) => listPage(request.params?.cursor)) + server.setRequestHandler('tools/call', async (request) => callResult(request.params.name)) + instrument(server, recorder.client, { logger: recorder.logger }) + return server + }, + { responseMode: 'json', onerror: (e) => recorder.logger(`handler error: ${e}`) } + ) + const node = toNodeHandler(handler) + const http = createServer((req, res) => node(req, res)).listen(0) + await sleep(300) + const port = http.address().port + try { + const first = await post( + port, + { + jsonrpc: '2.0', + id: 1, + method: 'tools/list', + params: { _meta: MODERN_META }, + }, + { 'mcp-method': 'tools/list' } + ) + const second = await post( + port, + { + jsonrpc: '2.0', + id: 2, + method: 'tools/list', + params: { cursor: first.result?.nextCursor, _meta: MODERN_META }, + }, + { 'mcp-method': 'tools/list' } + ) + const call = await post( + port, + { + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { name: 'page_two_tool', arguments: {}, _meta: MODERN_META }, + }, + { 'mcp-method': 'tools/call', 'mcp-name': 'page_two_tool' } + ) + await sleep(200) + assertEnumeration('v2', first.result, second.result, call.text) + } finally { + http.close() + await sleep(150) + } +} + +const MAJOR = (() => { + const i = process.argv.indexOf('--major') + return i === -1 ? 'all' : process.argv[i + 1] +})() + +if (MAJOR !== 'v2') await probeV1() +if (MAJOR !== 'v1') await probeV2() + +const passed = results.filter((r) => r.ok).length +console.log(`\n${passed}/${results.length} passed`) +process.exit(passed === results.length ? 0 : 1) diff --git a/packages/mcp/harness/dual-era/servers/v1.mjs b/packages/mcp/harness/dual-era/servers/v1.mjs new file mode 100644 index 0000000000..47c1be0d87 --- /dev/null +++ b/packages/mcp/harness/dual-era/servers/v1.mjs @@ -0,0 +1,111 @@ +// SDK v1 (@modelcontextprotocol/sdk) — the legacy era. +// +// PORT=0 (default) bind an ephemeral port; the chosen port is printed as +// `MCP_HARNESS_LISTENING port=` for the orchestrator. +// Set PORT explicitly for the single-cell debug flow. +// LEVEL=high|low high-level McpServer or low-level Server +// MODE=stateful one server + one transport; the TRANSPORT mints Mcp-Session-Id +// MODE=stateless a fresh server + transport PER REQUEST; @posthog/mcp mints its +// own self-encoded Mcp-Session-Id token at initialize, and the +// client replays it so each new instance recovers the session +// +// The stateless mode is what exercises session-token.ts. It is also the v1 analogue +// of v2's per-request factory: instrument() runs again on every request, so the +// WeakMap is empty each time and only the replayed token holds the session together. +// PR 8 changes this path. +import { createServer } from 'node:http' +import { randomUUID } from 'node:crypto' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' +import { instrument } from '@posthog/mcp' +import { TOOLS, TOOL_BY_NAME } from '../shared/tools.mjs' +import { createRecorder, handleInspectionRoute } from '../shared/posthog.mjs' + +const PORT = Number(process.env.PORT ?? 0) +const LEVEL = process.env.LEVEL === 'low' ? 'low' : 'high' +const MODE = process.env.MODE === 'stateless' ? 'stateless' : 'stateful' +const CONVERSATION_ID = process.env.CONVERSATION_ID === '1' + +const recorder = createRecorder(`v1:${LEVEL}:${MODE}`) + +function buildServer() { + if (LEVEL === 'high') { + const server = new McpServer({ name: 'testbed-v1', version: '1.0.0' }) + for (const t of TOOLS) { + server.registerTool(t.name, { description: t.description, inputSchema: t.inputShape }, t.handler) + } + return server + } + const server = new Server({ name: 'testbed-v1', version: '1.0.0' }, { capabilities: { tools: {} } }) + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.jsonSchema })), + })) + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const tool = TOOL_BY_NAME[request.params.name] + if (!tool) throw new Error(`Unknown tool: ${request.params.name}`) + return tool.handler(request.params.arguments ?? {}) + }) + return server +} + +function instrumented() { + const server = buildServer() + instrument(server, recorder.client, { logger: recorder.logger, enableConversationId: CONVERSATION_ID }) + return server +} + +async function readBody(req) { + if (req.method !== 'POST') return undefined + const chunks = [] + for await (const c of req) chunks.push(c) + const raw = Buffer.concat(chunks).toString('utf8') + return raw ? JSON.parse(raw) : undefined +} + +// ── stateful: one server for the whole process ────────────────────────────── +let sharedTransport +if (MODE === 'stateful') { + const server = instrumented() + sharedTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + enableJsonResponse: true, + }) + await server.connect(sharedTransport) +} + +createServer(async (req, res) => { + if (handleInspectionRoute(req, res, recorder)) return + let body + try { + body = await readBody(req) + } catch { + res.writeHead(400).end('bad json') + return + } + + if (MODE === 'stateful') { + await sharedTransport.handleRequest(req, res, body) + return + } + + // stateless: everything per request, as the v1 SDK prescribes. + // sessionIdGenerator: undefined + enableJsonResponse: true is the combination + // in which @posthog/mcp mints its own token into the response header. + const server = instrumented() + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }) + res.on('close', () => { + transport.close?.() + server.close?.() + }) + await server.connect(transport) + await transport.handleRequest(req, res, body) +}).listen(PORT, function () { + const port = this.address().port + console.log(`MCP_HARNESS_LISTENING port=${port}`) + console.log(`v1 ${LEVEL}/${MODE} on http://localhost:${port}/mcp conversationId=${CONVERSATION_ID}`) +}) diff --git a/packages/mcp/harness/dual-era/servers/v2.mjs b/packages/mcp/harness/dual-era/servers/v2.mjs new file mode 100644 index 0000000000..bbc7c56d5b --- /dev/null +++ b/packages/mcp/harness/dual-era/servers/v2.mjs @@ -0,0 +1,99 @@ +// SDK v2 (@modelcontextprotocol/server) — the 2026-07-28 era. +// +// PORT=0 (default) bind an ephemeral port; the chosen port is printed as +// `MCP_HARNESS_LISTENING port=` for the orchestrator. +// Set PORT explicitly for the single-cell debug flow. +// LEVEL=high|low high-level McpServer or low-level Server +// MODE=perrequest createMcpHandler — a FRESH server per HTTP request, with +// instrument() inside the factory. The 2026 topology. +// MODE=longlived one server for the process on NodeStreamableHTTPServerTransport. +// +// The longlived mode is the control. Without it, every v2 failure is confounded: +// you cannot tell whether a result is caused by the SDK major or by the server +// being rebuilt each request. Run both and the difference is the answer. +import { createServer } from 'node:http' +import { randomUUID } from 'node:crypto' +import { McpServer, Server, createMcpHandler } from '@modelcontextprotocol/server' +import { NodeStreamableHTTPServerTransport, toNodeHandler } from '@modelcontextprotocol/node' +import { instrument } from '@posthog/mcp' +import { TOOLS, TOOL_BY_NAME } from '../shared/tools.mjs' +import { createRecorder, handleInspectionRoute } from '../shared/posthog.mjs' + +const PORT = Number(process.env.PORT ?? 0) +const LEVEL = process.env.LEVEL === 'low' ? 'low' : 'high' +const MODE = process.env.MODE === 'longlived' ? 'longlived' : 'perrequest' +const CONVERSATION_ID = process.env.CONVERSATION_ID === '1' +// Registers a custom method through v2's 3-argument form AFTER instrument(). +// This is the standing regression assertion: today it throws and takes the +// server's request down with it. +const CUSTOM_3ARG = process.env.CUSTOM_3ARG === '1' +// longlived only: mint a transport session id (the legacy stateful deployment). +const STATEFUL = process.env.STATEFUL === '1' + +const recorder = createRecorder(`v2:${LEVEL}:${MODE}`) + +function buildServer() { + if (LEVEL === 'high') { + const server = new McpServer({ name: 'testbed-v2', version: '1.0.0' }) + for (const t of TOOLS) { + server.registerTool(t.name, { description: t.description, inputSchema: t.inputShape }, t.handler) + } + return server + } + const server = new Server({ name: 'testbed-v2', version: '1.0.0' }, { capabilities: { tools: {} } }) + server.setRequestHandler('tools/list', async () => ({ + tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.jsonSchema })), + })) + server.setRequestHandler('tools/call', async (request) => { + const tool = TOOL_BY_NAME[request.params.name] + if (!tool) throw new Error(`Unknown tool: ${request.params.name}`) + return tool.handler(request.params.arguments ?? {}) + }) + return server +} + +function instrumented() { + const server = buildServer() + instrument(server, recorder.client, { logger: recorder.logger, enableConversationId: CONVERSATION_ID }) + if (CUSTOM_3ARG) { + // Must survive instrument(). Our patched setRequestHandler forwards only + // two arguments, so v2's 3-argument custom form is rejected. + const low = server.server ?? server + low.setRequestHandler('testbed/custom', { paramsSchema: undefined, resultSchema: undefined }, async () => ({ + ok: true, + })) + } + return server +} + +let mcp +if (MODE === 'perrequest') { + const handler = createMcpHandler(instrumented, { + responseMode: 'json', + onerror: (e) => recorder.logger(`handler error: ${e}`), + }) + mcp = toNodeHandler(handler) +} else { + // One server for the process. sessionIdGenerator gives it the legacy stateful + // behaviour a v2 operator opts into — the only way ctx.sessionId is ever set. + const server = instrumented() + const transport = new NodeStreamableHTTPServerTransport({ + // undefined ⇒ stateless: no handshake required, so the modern era works + // with a long-lived server. That is the control for the per-request case. + sessionIdGenerator: STATEFUL ? () => randomUUID() : undefined, + responseMode: 'json', + }) + await server.connect(transport) + mcp = (req, res) => transport.handleRequest(req, res) +} + +createServer(async (req, res) => { + if (handleInspectionRoute(req, res, recorder)) return + await mcp(req, res) +}).listen(PORT, function () { + const port = this.address().port + console.log(`MCP_HARNESS_LISTENING port=${port}`) + console.log( + `v2 ${LEVEL}/${MODE} on http://localhost:${port}/ conversationId=${CONVERSATION_ID} custom3arg=${CUSTOM_3ARG}` + ) +}) diff --git a/packages/mcp/harness/dual-era/shared/posthog.mjs b/packages/mcp/harness/dual-era/shared/posthog.mjs new file mode 100644 index 0000000000..06d55f5992 --- /dev/null +++ b/packages/mcp/harness/dual-era/shared/posthog.mjs @@ -0,0 +1,74 @@ +// A posthog-node stand-in that records events in memory and prints one line each. +// No API key, no network — the assertions read the recorded array over /__events. + +const RESET = '\x1b[0m' +const DIM = '\x1b[2m' + +export function createRecorder(label) { + const events = [] + const warnings = [] + + const summarise = (e) => { + const p = e.properties ?? {} + return [ + e.event, + p.$mcp_tool_name ? `tool=${p.$mcp_tool_name}` : null, + p.$session_id ? `session=${String(p.$session_id).slice(0, 12)}…` : null, + p.$mcp_client_name ? `client=${p.$mcp_client_name}` : null, + p.$mcp_is_error ? 'ERROR' : null, + ] + .filter(Boolean) + .join(' ') + } + + const record = (event) => { + events.push(event) + console.log(`${DIM}[${label}]${RESET} ${summarise(event)}`) + } + + const client = { + capture: (event) => record(event), + identify: (event) => record({ ...event, event: '$identify' }), + captureException: (error, distinctId, properties) => + record({ event: '$exception', distinctId, properties: { ...properties, message: String(error) } }), + flush: async () => {}, + shutdown: async () => {}, + } + + return { + client, + events, + warnings, + // instrument() reports every internal failure through its logger; a testbed + // that ignores those would miss exactly the silent-degradation bug this + // harness exists to catch. + logger: (...args) => { + const line = args.join(' ') + // Match the SDK's own prefixes only. A substring match on /fail/ would + // flag the routine "Tool fail_always callback already wrapped" notice. + if (/^(Warning|Error):|compatibility error|Failed to /.test(line)) warnings.push(line) + console.log(`${DIM}[${label}:sdk]${RESET} ${line}`) + }, + reset: () => { + events.length = 0 + warnings.length = 0 + }, + } +} + +/** Serves the recorded state so the out-of-process client can assert on it. */ +export function handleInspectionRoute(req, res, recorder) { + const url = new URL(req.url, 'http://localhost') + if (url.pathname === '/__events') { + res.writeHead(200, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ events: recorder.events, warnings: recorder.warnings })) + return true + } + if (url.pathname === '/__reset') { + recorder.reset() + res.writeHead(200, { 'content-type': 'application/json' }) + res.end('{"ok":true}') + return true + } + return false +} diff --git a/packages/mcp/harness/dual-era/shared/tools.mjs b/packages/mcp/harness/dual-era/shared/tools.mjs new file mode 100644 index 0000000000..97f8777bcf --- /dev/null +++ b/packages/mcp/harness/dual-era/shared/tools.mjs @@ -0,0 +1,44 @@ +// The three tools, defined once and registered by both servers through their own SDK. +// Each exists to exercise exactly one capture path. +// +// zod4 is the workspace alias for zod v4: the v2 SDK requires zod-4 schemas, and the +// v1 SDK (>=1.30) accepts them through its compat layer — one schema form serves both. +// packages/mcp's real-name zod stays v3 for the unit suite. +import { z } from 'zod4' + +export const TOOLS = [ + { + name: 'echo', + description: 'Echoes the text back. The happy path.', + // Raw Zod shape — both SDK majors accept this form for registerTool(). + inputShape: { text: z.string().describe('Text to echo back') }, + jsonSchema: { + type: 'object', + properties: { text: { type: 'string', description: 'Text to echo back' } }, + required: ['text'], + }, + handler: async (args) => ({ content: [{ type: 'text', text: String(args?.text ?? '') }] }), + }, + { + name: 'add', + description: 'Adds two numbers. A second, different tool — proves session continuity across tools.', + inputShape: { a: z.number(), b: z.number() }, + jsonSchema: { + type: 'object', + properties: { a: { type: 'number' }, b: { type: 'number' } }, + required: ['a', 'b'], + }, + handler: async (args) => ({ content: [{ type: 'text', text: String(Number(args?.a) + Number(args?.b)) }] }), + }, + { + name: 'fail_always', + description: 'Always throws. Exercises error capture and $exception.', + inputShape: {}, + jsonSchema: { type: 'object', properties: {} }, + handler: async () => { + throw new Error('intentional failure') + }, + }, +] + +export const TOOL_BY_NAME = Object.fromEntries(TOOLS.map((t) => [t.name, t])) diff --git a/packages/mcp/harness/nest-v1/src/analytics.tool.ts b/packages/mcp/harness/nest-v1/src/analytics.tool.ts new file mode 100644 index 0000000000..57fdca72f3 --- /dev/null +++ b/packages/mcp/harness/nest-v1/src/analytics.tool.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common' +import { Tool } from 'rekog-mcp-nest-v1' +// zod4 is the workspace alias for zod v4: mcp-nest 1.9 expects zod-4 schemas +// (validated by this copy via safeParse; converted to JSON Schema by the v1 +// SDK's compat layer). packages/mcp's real-name zod stays v3 for the unit suite. +import { z } from 'zod4' + +/** + * The same three tools as the v2 harness — `echo` / `add` / `fail_always` — so the + * two runs are directly comparable. Defined the mcp-nest 1.x way: an `@Injectable` + * provider with `@Tool` methods. + */ +@Injectable() +export class AnalyticsTool { + @Tool({ + name: 'echo', + description: 'Echoes the text back. The happy path.', + parameters: z.object({ text: z.string().describe('Text to echo back') }), + }) + async echo({ text }: { text: string }) { + return { content: [{ type: 'text', text: String(text ?? '') }] } + } + + @Tool({ + name: 'add', + description: 'Adds two numbers. A second tool — proves session continuity across tools.', + parameters: z.object({ a: z.number(), b: z.number() }), + }) + async add({ a, b }: { a: number; b: number }) { + return { content: [{ type: 'text', text: String(Number(a) + Number(b)) }] } + } + + @Tool({ + name: 'fail_always', + description: 'Always throws. Exercises error capture and $exception.', + parameters: z.object({}), + }) + async failAlways() { + throw new Error('intentional failure') + } +} diff --git a/packages/mcp/harness/nest-v1/src/app.module.ts b/packages/mcp/harness/nest-v1/src/app.module.ts new file mode 100644 index 0000000000..f561eb4b68 --- /dev/null +++ b/packages/mcp/harness/nest-v1/src/app.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common' +import { McpModule, McpTransportType } from 'rekog-mcp-nest-v1' +import { AnalyticsTool } from './analytics.tool' +import { instrumentationMutator, instrumentationMutatorLowLevel } from './posthog' + +const LEVEL = process.env.LEVEL === 'low' ? 'low' : 'high' + +@Module({ + imports: [ + McpModule.forRoot({ + name: 'nest-v1-acceptance', + version: '1.0.0', + transport: McpTransportType.STREAMABLE_HTTP, + streamableHttp: { + // The same topology as the v2 harness: a fresh server per HTTP request. + statelessMode: true, + // Required for the minted Mcp-Session-Id token to reach the client — on + // SSE the headers are flushed before the handler runs. + enableJsonResponse: true, + }, + logging: false, + serverMutator: LEVEL === 'low' ? instrumentationMutatorLowLevel : instrumentationMutator, + }), + ], + providers: [AnalyticsTool], +}) +export class AppModule {} diff --git a/packages/mcp/harness/nest-v1/src/main.ts b/packages/mcp/harness/nest-v1/src/main.ts new file mode 100644 index 0000000000..e297ce4a98 --- /dev/null +++ b/packages/mcp/harness/nest-v1/src/main.ts @@ -0,0 +1,25 @@ +import 'reflect-metadata' +import { NestFactory } from '@nestjs/core' +import type { Request, Response } from 'express' +import { AppModule } from './app.module' +import { events, warnings, reset } from './posthog' + +async function bootstrap() { + const app = await NestFactory.create(AppModule, { logger: ['error'] }) + + const http = app.getHttpAdapter().getInstance() + http.get('/__events', (_req: Request, res: Response) => res.json({ events, warnings })) + http.get('/__reset', (_req: Request, res: Response) => { + reset() + res.json({ ok: true }) + }) + + // PORT=0 (the default) binds an ephemeral port; the chosen port is announced + // on stdout for the verifier. Set PORT explicitly for manual runs. + const server = await app.listen(Number(process.env.PORT ?? 0)) + const port = (server.address() as { port: number }).port + console.log(`MCP_HARNESS_LISTENING port=${port}`) + console.error(`server.ready http://localhost:${port}/mcp LEVEL=${process.env.LEVEL ?? 'high'} (v1, stateless)`) +} + +void bootstrap() diff --git a/packages/mcp/harness/nest-v1/src/posthog.ts b/packages/mcp/harness/nest-v1/src/posthog.ts new file mode 100644 index 0000000000..bc794576db --- /dev/null +++ b/packages/mcp/harness/nest-v1/src/posthog.ts @@ -0,0 +1,105 @@ +// A posthog-node stand-in that records events in memory. No API key, no network — +// verify.mjs reads the recorded array back over /__events. +// +// The logger matters as much as the events. `instrument()` reports every internal +// failure through it and otherwise returns a healthy-looking handle, which is the +// exact silent-degradation this harness exists to catch. + +import { instrument } from '@posthog/mcp' + +export interface RecordedEvent { + event: string + distinctId?: string + properties?: Record +} + +export const events: RecordedEvent[] = [] +export const warnings: string[] = [] + +const DIM = '\x1b[2m' +const RESET = '\x1b[0m' + +function summarise(e: RecordedEvent): string { + const p = e.properties ?? {} + return [ + e.event, + p.$mcp_tool_name ? `tool=${p.$mcp_tool_name}` : null, + p.$session_id ? `session=${String(p.$session_id).slice(0, 12)}…` : null, + p.$mcp_client_name ? `client=${p.$mcp_client_name}` : null, + p.$mcp_protocol_version ? `proto=${p.$mcp_protocol_version}` : null, + p.$mcp_intent ? `intent=${JSON.stringify(p.$mcp_intent)}` : null, + p.$mcp_is_error ? 'ERROR' : null, + ] + .filter(Boolean) + .join(' ') +} + +function record(event: RecordedEvent): void { + events.push(event) + console.log(`${DIM}[posthog]${RESET} ${summarise(event)}`) +} + +const client = { + capture: (event: RecordedEvent) => record(event), + identify: (event: RecordedEvent) => record({ ...event, event: '$identify' }), + captureException: (error: unknown, distinctId?: string, properties?: Record) => + record({ event: '$exception', distinctId, properties: { ...properties, message: String(error) } }), + flush: async () => {}, + shutdown: async () => {}, +} + +const logger = (...args: unknown[]): void => { + const line = args.join(' ') + // Match the SDK's own prefixes only, so routine notices are not counted as + // failures. A substring match on /fail/ would flag "Tool fail_always callback + // already wrapped". + if (/^(Warning|Error):|compatibility error|Failed to /.test(line)) warnings.push(line) + console.log(`${DIM}[posthog:sdk]${RESET} ${line}`) +} + +export function reset(): void { + events.length = 0 + warnings.length = 0 +} + +/** + * `identify` deliberately reads a header, and reads it the **v1 way**. + * + * Same callback as the v2 harness, so the two are directly comparable. Here it is + * reading the shape the SDK actually provides, so it must resolve a user — that + * is the point of running it on v1. + */ +function identifyFromHeader(_request: unknown, extra: any) { + const auth = extra?.requestInfo?.headers?.['authorization'] ?? extra?.requestInfo?.headers?.['Authorization'] + if (typeof auth !== 'string') return null + const token = auth.replace(/^Bearer\s+/i, '') + return { distinctId: `user_${token}`, properties: { token } } +} + +/** + * The integration under test. mcp-nest 1.x calls this with the high-level SDK v1 + * `McpServer`, then binds its handlers afterwards with Zod request schemas. + * + * This is the v1 no-regression guard for the NestJS adapter: these numbers must + * not move when v2 support lands. + */ +export const instrumentationMutator = (server: any) => { + instrument(server, client as any, { + logger, + context: true, + enableConversationId: true, + identify: identifyFromHeader as any, + }) + return server +} + +/** Same, but through the `instrument(server.server)` workaround users adopted. */ +export const instrumentationMutatorLowLevel = (server: any) => { + instrument(server.server, client as any, { + logger, + context: true, + enableConversationId: true, + identify: identifyFromHeader as any, + }) + return server +} diff --git a/packages/mcp/harness/nest-v1/tsconfig.json b/packages/mcp/harness/nest-v1/tsconfig.json new file mode 100644 index 0000000000..3a1c72f21a --- /dev/null +++ b/packages/mcp/harness/nest-v1/tsconfig.json @@ -0,0 +1,20 @@ +{ + "ts-node": { + "transpileOnly": true, + "compilerOptions": { "module": "commonjs" } + }, + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": false, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/mcp/harness/nest-v1/verify.mjs b/packages/mcp/harness/nest-v1/verify.mjs new file mode 100644 index 0000000000..1682b46d9d --- /dev/null +++ b/packages/mcp/harness/nest-v1/verify.mjs @@ -0,0 +1,235 @@ +// v1 no-regression guard for the NestJS adapter: +// NestJS + @rekog/mcp-nest 1.9.x + MCP SDK v1, stateless streamable HTTP. +// +// Same assertions and same tools as the v2 harness, so the two are directly +// comparable. SDK v1 is frozen at protocol 2025-11-25, so there is no modern lane. +// +// node verify.mjs # LEVEL=high — instrument(server), as documented +// LEVEL=low node verify.mjs # the instrument(server.server) workaround +// +// The server binds an ephemeral port (PORT=0) and announces it on stdout; set +// PORT explicitly to pin one for manual runs. +// +// Raw JSON-RPC over fetch, never the SDK Client — a stock v2 Client negotiates +// the LEGACY era and would report green having tested none of 2026-07-28. +import { spawn } from 'node:child_process' +import { setTimeout as sleep } from 'node:timers/promises' +import { fileURLToPath } from 'node:url' + +const DIR = fileURLToPath(new URL('.', import.meta.url)) +const TS_NODE = fileURLToPath(new URL('../../node_modules/.bin/ts-node', import.meta.url)) +const EXPLICIT_PORT = process.env.PORT ? Number(process.env.PORT) : null +const LEVEL = process.env.LEVEL === 'low' ? 'low' : 'high' +const TOKEN = 'acceptance-token' +let BASE + +const GREEN = '\x1b[32m' +const RED = '\x1b[31m' +const DIM = '\x1b[2m' +const RESET = '\x1b[0m' + +const results = [] +function check(name, ok, detail = '') { + results.push({ name, ok }) + console.log(` ${ok ? GREEN + '✓' : RED + '✗'}${RESET} ${name}${detail ? ` ${DIM}${detail}${RESET}` : ''}`) +} + +async function waitUp() { + // ts-node compiles the Nest graph on boot; be generous. + for (let i = 0; i < 480; i++) { + try { + await fetch(`${BASE}/__events`) + return true + } catch { + await sleep(250) + } + } + return false +} + +const readState = async () => (await fetch(`${BASE}/__events`)).json() +const resetState = () => fetch(`${BASE}/__reset`) + +/** A 2025-era (legacy) request: an initialize handshake, no _meta envelope. */ +function legacyHeaders() { + return { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + authorization: `Bearer ${TOKEN}`, + } +} + +// A real client replays the Mcp-Session-Id it was handed on every later request. +// Without this the harness reports a session/identity failure that no real client +// would see: on a stateless server the replayed token is the ONLY thing carrying +// client info and the negotiated protocol version between per-request instances. +let sessionId + +let id = 0 +async function rpc(method, params = {}) { + const body = { jsonrpc: '2.0', id: ++id, method, params } + const headers = legacyHeaders() + if (sessionId) headers['mcp-session-id'] = sessionId + const res = await fetch(`${BASE}/mcp`, { method: 'POST', headers, body: JSON.stringify(body) }) + const minted = res.headers.get('mcp-session-id') + if (minted) sessionId = minted + const text = await res.text() + let json + try { + json = JSON.parse(text) + } catch { + // SSE framing — pull the data: line out + const line = text.split('\n').find((l) => l.startsWith('data:')) + json = line ? JSON.parse(line.slice(5).trim()) : { raw: text.slice(0, 200) } + } + return { status: res.status, headers: res.headers, json } +} + +async function runLegacy() { + console.log(`\n2025-11-25 (legacy) · LEVEL=${LEVEL}`) + sessionId = undefined + await resetState() + + await rpc('initialize', { + protocolVersion: '2025-11-25', + capabilities: {}, + clientInfo: { name: 'acceptance-client', version: '9.9' }, + }) + + const list = await rpc('tools/list') + const tools = list.json?.result?.tools ?? [] + const echoTool = tools.find((t) => t.name === 'echo') + + const call = await rpc('tools/call', { name: 'echo', arguments: { text: 'hi' } }) + await rpc('tools/call', { name: 'fail_always', arguments: {} }) + + await sleep(150) + const { events, warnings } = await readState() + const toolCalls = events.filter((e) => e.event === '$mcp_tool_call') + const listings = events.filter((e) => e.event === '$mcp_tools_list') + const echoCall = toolCalls.find((e) => e.properties?.$mcp_tool_name === 'echo') + const failCall = toolCalls.find((e) => e.properties?.$mcp_tool_name === 'fail_always') + + // --- the server still works at all (regression floor) --- + check('host server answers tools/list', list.status === 200 && tools.length > 0, `${tools.length} tools`) + check('host server answers tools/call', call.status === 200, `HTTP ${call.status}`) + check('tool result is correct', call.json?.result?.content?.[0]?.text === 'hi') + + // --- it captures at all --- + check('instrument() logged no warning', warnings.length === 0, warnings[0] ?? '') + check('$mcp_tools_list captured', listings.length >= 1, `got ${listings.length}`) + check('$mcp_tool_call captured for echo', !!echoCall) + check('$mcp_tool_call captured for fail_always', !!failCall) + check('error call recorded as an error', failCall?.properties?.$mcp_is_error === true) + check( + 'error message is clean', + failCall?.properties?.$mcp_error_message === 'intentional failure', + JSON.stringify(failCall?.properties?.$mcp_error_message) + ) + + // --- the injected context parameter reaches the wire --- + check('context parameter advertised on tools/list', !!echoTool?.inputSchema?.properties?.context) + check('conversation_id advertised on tools/list', !!echoTool?.inputSchema?.properties?.conversation_id) + + // --- identity --- + check('identify() resolved a user', echoCall?.distinctId === `user_${TOKEN}`, `distinct_id=${echoCall?.distinctId}`) + check('client name recorded', !!echoCall?.properties?.$mcp_client_name, echoCall?.properties?.$mcp_client_name) + check( + 'protocol version recorded', + !!echoCall?.properties?.$mcp_protocol_version, + echoCall?.properties?.$mcp_protocol_version + ) + + // --- ownership on a per-request instance --- + const intentCall = await rpc('tools/call', { + name: 'echo', + arguments: { text: 'hi', context: 'checking adoption numbers' }, + }) + await sleep(150) + const after = await readState() + const withIntent = after.events.filter((e) => e.event === '$mcp_tool_call').find((e) => e.properties?.$mcp_intent) + check( + '$mcp_intent captured from the context argument', + withIntent?.properties?.$mcp_intent === 'checking adoption numbers', + JSON.stringify(withIntent?.properties?.$mcp_intent) + ) + check( + 'context argument stripped before the tool ran', + intentCall.json?.result?.content?.[0]?.text === 'hi', + JSON.stringify(intentCall.json?.result?.content?.[0]?.text) + ) +} + +// With an explicit PORT, fail fast rather than silently assert against someone +// else's server — a stale process holding the port may have booted with a +// different LEVEL. (Meaningless for the ephemeral default.) +if (EXPLICIT_PORT) { + try { + await fetch(`http://localhost:${EXPLICIT_PORT}/__events`) + console.error(`port ${EXPLICIT_PORT} is already serving. Kill it first before verifying.`) + process.exit(1) + } catch { + // nothing listening — good + } +} + +// Spawn the binary directly, not through a runner: an extra process layer would +// orphan the ts-node grandchild that actually holds the port when killed. +// `detached` puts the child in its own group so the whole tree dies with it. +const child = spawn(TS_NODE, ['src/main.ts'], { + cwd: DIR, + env: { ...process.env, PORT: String(EXPLICIT_PORT ?? 0), LEVEL }, + stdio: ['ignore', 'pipe', 'inherit'], + detached: true, +}) + +function killServer() { + try { + process.kill(-child.pid, 'SIGKILL') + } catch { + try { + child.kill('SIGKILL') + } catch { + /* already gone */ + } + } +} +process.on('exit', killServer) +for (const sig of ['SIGINT', 'SIGTERM']) { + process.on(sig, () => { + killServer() + process.exit(1) + }) +} + +// The server announces its (possibly ephemeral) port on stdout. +const port = await new Promise((resolve) => { + let out = '' + child.stdout.on('data', (d) => { + process.stdout.write(d) + out += d + const m = out.match(/MCP_HARNESS_LISTENING port=(\d+)/) + if (m) resolve(Number(m[1])) + }) + child.on('exit', () => resolve(null)) + setTimeout(() => resolve(null), 120000) +}) +if (!port) { + console.error('server never announced a port') + killServer() + process.exit(1) +} +BASE = `http://localhost:${port}` + +if (!(await waitUp())) { + console.error('server never came up') + killServer() + process.exit(1) +} + +await runLegacy() + +const passed = results.filter((r) => r.ok).length +console.log(`\n${passed}/${results.length} passed`) +killServer() +process.exit(passed === results.length ? 0 : 1) diff --git a/packages/mcp/harness/nest-v2/expected-failures.json b/packages/mcp/harness/nest-v2/expected-failures.json new file mode 100644 index 0000000000..e844677e7f --- /dev/null +++ b/packages/mcp/harness/nest-v2/expected-failures.json @@ -0,0 +1,12 @@ +[ + { + "era": "legacy", + "name": "error message is clean", + "why": "NestJS RpcExceptionsHandler replaces the thrown error with 'Internal server error' before the MCP layer sees it — mcp-nest/NestJS behaviour, not an @posthog/mcp bug" + }, + { + "era": "modern", + "name": "error message is clean", + "why": "Same NestJS error flattening as the legacy era" + } +] diff --git a/packages/mcp/harness/nest-v2/src/analytics.controller.ts b/packages/mcp/harness/nest-v2/src/analytics.controller.ts new file mode 100644 index 0000000000..92b598ae21 --- /dev/null +++ b/packages/mcp/harness/nest-v2/src/analytics.controller.ts @@ -0,0 +1,54 @@ +import { McpController, Tool } from 'rekog-mcp-nest-v2' +import { Payload } from '@nestjs/microservices' + +/** + * Three tools, mirroring the dual-era testbed's `echo` / `add` / `fail_always` + * so results are comparable across harnesses. Defined the mcp-nest 2.0 way — an + * `@McpController` with `@Tool` methods, discovered and bound by the strategy. + * + * Parameters are raw JSON Schema objects — one of mcp-nest 2.0's supported + * schema forms, and the one that keeps this fixture independent of which zod + * major mcp-nest's own zod import resolves to (its zod-typed path calls + * `z.toJSONSchema`, a zod-4-only API). + * + * Note what this does *not* do: it never calls `registerTool()`. mcp-nest binds + * `tools/list` and `tools/call` directly on the low-level server, so + * `_registeredTools` stays empty on the high-level `McpServer` we are handed. + */ +@McpController() +export class AnalyticsController { + @Tool({ + name: 'echo', + description: 'Echoes the text back. The happy path.', + parameters: { + type: 'object', + properties: { text: { type: 'string', description: 'Text to echo back' } }, + required: ['text'], + }, + }) + async echo(@Payload() { text }: { text: string }) { + return { content: [{ type: 'text', text: String(text ?? '') }] } + } + + @Tool({ + name: 'add', + description: 'Adds two numbers. A second tool — proves session continuity across tools.', + parameters: { + type: 'object', + properties: { a: { type: 'number' }, b: { type: 'number' } }, + required: ['a', 'b'], + }, + }) + async add(@Payload() { a, b }: { a: number; b: number }) { + return { content: [{ type: 'text', text: String(Number(a) + Number(b)) }] } + } + + @Tool({ + name: 'fail_always', + description: 'Always throws. Exercises error capture and $exception.', + parameters: { type: 'object', properties: {} }, + }) + async failAlways() { + throw new Error('intentional failure') + } +} diff --git a/packages/mcp/harness/nest-v2/src/app.module.ts b/packages/mcp/harness/nest-v2/src/app.module.ts new file mode 100644 index 0000000000..6826b6d498 --- /dev/null +++ b/packages/mcp/harness/nest-v2/src/app.module.ts @@ -0,0 +1,36 @@ +import { Module } from '@nestjs/common' +import { McpStrategy, MCP_STRATEGY, StreamableHttpTransport } from 'rekog-mcp-nest-v2' +import { AnalyticsController } from './analytics.controller' +import { instrumentationMutator, instrumentationMutatorLowLevel } from './posthog' + +/** + * `LEVEL=low` uses the `instrument(server.server)` workaround users adopted to get + * past the compatibility gate. Both are kept so we can measure whether the + * workaround actually costs them anything on this adapter. + */ +const LEVEL = process.env.LEVEL === 'low' ? 'low' : 'high' + +/** + * `dual` serves both protocol eras off one endpoint, which is what a real v2 + * deployment looks like — clients migrate slower than servers. `statefulMode: + * false` is the reported topology: a fresh `McpServer` per HTTP request. + */ +export const mcp = new McpStrategy({ + name: 'nest-v2-acceptance', + version: '1.0.0', + transports: [ + new StreamableHttpTransport({ + endpoint: '/mcp', + statefulMode: false, + enableJsonResponse: true, + protocol: process.env.PROTOCOL === 'legacy-only' ? 'legacy-only' : 'dual', + }), + ], + serverMutator: LEVEL === 'low' ? instrumentationMutatorLowLevel : instrumentationMutator, +}) + +@Module({ + controllers: [AnalyticsController], + providers: [{ provide: MCP_STRATEGY, useValue: mcp }], +}) +export class AppModule {} diff --git a/packages/mcp/harness/nest-v2/src/main.ts b/packages/mcp/harness/nest-v2/src/main.ts new file mode 100644 index 0000000000..493b9d7ec0 --- /dev/null +++ b/packages/mcp/harness/nest-v2/src/main.ts @@ -0,0 +1,31 @@ +import 'reflect-metadata' +import { NestFactory } from '@nestjs/core' +import type { Request, Response } from 'express' +import { AppModule, mcp } from './app.module' +import { events, warnings, extraShapes, reset } from './posthog' + +async function bootstrap() { + const app = await NestFactory.create(AppModule, { logger: ['error', 'warn'] }) + + // Inspection routes, so verify.mjs can assert out of process. + const http = app.getHttpAdapter().getInstance() + http.get('/__events', (_req: Request, res: Response) => res.json({ events, warnings, extraShapes })) + http.get('/__reset', (_req: Request, res: Response) => { + reset() + res.json({ ok: true }) + }) + + mcp.setHttpAdapter(app.getHttpAdapter()) + app.connectMicroservice({ strategy: mcp }) + // Order matters: MCP routes mount here, before listen() accepts connections. + await app.startAllMicroservices() + + // PORT=0 (the default) binds an ephemeral port; the chosen port is announced + // on stdout for the verifier. Set PORT explicitly for manual runs. + const server = await app.listen(Number(process.env.PORT ?? 0)) + const port = (server.address() as { port: number }).port + console.log(`MCP_HARNESS_LISTENING port=${port}`) + console.error(`server.ready http://localhost:${port}/mcp LEVEL=${process.env.LEVEL ?? 'high'} (stateless, dual-era)`) +} + +void bootstrap() diff --git a/packages/mcp/harness/nest-v2/src/posthog.ts b/packages/mcp/harness/nest-v2/src/posthog.ts new file mode 100644 index 0000000000..be5e8c6fb2 --- /dev/null +++ b/packages/mcp/harness/nest-v2/src/posthog.ts @@ -0,0 +1,138 @@ +// A posthog-node stand-in that records events in memory. No API key, no network — +// verify.mjs reads the recorded array back over /__events. +// +// The logger matters as much as the events. `instrument()` reports every internal +// failure through it and otherwise returns a healthy-looking handle, which is the +// exact silent-degradation this harness exists to catch. + +import { instrument, getRequestHeaders } from '@posthog/mcp' + +export interface RecordedEvent { + event: string + distinctId?: string + properties?: Record +} + +export const events: RecordedEvent[] = [] +export const warnings: string[] = [] + +/** + * What `extra` actually looked like each time a host callback received it. + * + * ADR-0006 says we hand host callbacks the SDK's own `extra`, unnormalised — so + * on v2 there must be **no** `requestInfo`, because synthesising one would be a + * convincing partial lie. That used to be proved indirectly, by leaving + * `identify` v1-shaped and letting it fail; this records the shape instead, so + * the invariant is asserted positively and `identify` is free to be correct. + */ +export interface ObservedExtraShape { + hasRequestInfo: boolean + hasHttpReq: boolean + headersResolved: boolean +} +export const extraShapes: ObservedExtraShape[] = [] + +const DIM = '\x1b[2m' +const RESET = '\x1b[0m' + +function summarise(e: RecordedEvent): string { + const p = e.properties ?? {} + return [ + e.event, + p.$mcp_tool_name ? `tool=${p.$mcp_tool_name}` : null, + p.$session_id ? `session=${String(p.$session_id).slice(0, 12)}…` : null, + p.$mcp_client_name ? `client=${p.$mcp_client_name}` : null, + p.$mcp_protocol_version ? `proto=${p.$mcp_protocol_version}` : null, + p.$mcp_intent ? `intent=${JSON.stringify(p.$mcp_intent)}` : null, + p.$mcp_is_error ? 'ERROR' : null, + ] + .filter(Boolean) + .join(' ') +} + +function record(event: RecordedEvent): void { + events.push(event) + console.log(`${DIM}[posthog]${RESET} ${summarise(event)}`) +} + +const client = { + capture: (event: RecordedEvent) => record(event), + identify: (event: RecordedEvent) => record({ ...event, event: '$identify' }), + captureException: (error: unknown, distinctId?: string, properties?: Record) => + record({ event: '$exception', distinctId, properties: { ...properties, message: String(error) } }), + flush: async () => {}, + shutdown: async () => {}, +} + +const logger = (...args: unknown[]): void => { + const line = args.join(' ') + // Match the SDK's own prefixes only, so routine notices are not counted as + // failures. A substring match on /fail/ would flag "Tool fail_always callback + // already wrapped". + if (/^(Warning|Error):|compatibility error|Failed to /.test(line)) warnings.push(line) + console.log(`${DIM}[posthog:sdk]${RESET} ${line}`) +} + +export function reset(): void { + events.length = 0 + warnings.length = 0 + extraShapes.length = 0 +} + +/** + * `identify` reads a header through the **exported** `getRequestHeaders`, which + * is the migration the SDK documents for hosts moving to v2 — one line instead + * of a hand-written two-branch read that gets case-insensitivity, array values + * and the cross-realm `Headers` check wrong. + * + * This used to be left v1-shaped (`extra.requestInfo.headers`) so that its + * failure would stand as the ADR-0006 assertion. That cost four assertions + * across two eras and proved the invariant only by inference. The invariant is + * now asserted directly from `extraShapes` below, which frees this callback to + * do the correct thing — and makes it the only end-to-end coverage that + * `getRequestHeaders` actually resolves headers on a v2 server. + * + * `IDENTIFY=legacy` restores the old v1-shaped read, for reproducing what a + * host sees before they migrate. + */ +function identifyFromHeader(_request: unknown, extra: any) { + extraShapes.push({ + hasRequestInfo: !!extra?.requestInfo, + hasHttpReq: !!extra?.http?.req, + headersResolved: !!getRequestHeaders(extra), + }) + const auth = + process.env.IDENTIFY === 'legacy' + ? (extra?.requestInfo?.headers?.['authorization'] ?? extra?.requestInfo?.headers?.['Authorization']) + : getRequestHeaders(extra)?.['authorization'] + if (typeof auth !== 'string') return null + const token = auth.replace(/^Bearer\s+/i, '') + return { distinctId: `user_${token}`, properties: { token } } +} + +/** + * The integration under test. mcp-nest calls this with the **high-level v2 + * `McpServer`**, then binds `tools/list` / `tools/call` afterwards via + * `server.server.setRequestHandler('tools/list', …)` — string method names, on + * the low-level server, after instrumentation. + */ +export const instrumentationMutator = (server: any) => { + instrument(server, client as any, { + logger, + context: true, + enableConversationId: true, + identify: identifyFromHeader as any, + }) + return server +} + +/** Same, but through the `instrument(server.server)` workaround users adopted. */ +export const instrumentationMutatorLowLevel = (server: any) => { + instrument(server.server, client as any, { + logger, + context: true, + enableConversationId: true, + identify: identifyFromHeader as any, + }) + return server +} diff --git a/packages/mcp/harness/nest-v2/tsconfig.json b/packages/mcp/harness/nest-v2/tsconfig.json new file mode 100644 index 0000000000..3a1c72f21a --- /dev/null +++ b/packages/mcp/harness/nest-v2/tsconfig.json @@ -0,0 +1,20 @@ +{ + "ts-node": { + "transpileOnly": true, + "compilerOptions": { "module": "commonjs" } + }, + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": false, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/mcp/harness/nest-v2/verify.mjs b/packages/mcp/harness/nest-v2/verify.mjs new file mode 100644 index 0000000000..7fccedb531 --- /dev/null +++ b/packages/mcp/harness/nest-v2/verify.mjs @@ -0,0 +1,310 @@ +// Acceptance test for the stack reported in posthog-js#4449: +// NestJS + @rekog/mcp-nest 2.0.0 + MCP SDK v2, stateless streamable HTTP. +// +// node verify.mjs # both eras, LEVEL=high +// LEVEL=low node verify.mjs # the instrument(server.server) workaround +// +// The server binds an ephemeral port (PORT=0) and announces it on stdout; set +// PORT explicitly to pin one for manual runs. +// +// Known-broken assertions live in expected-failures.json. The run passes iff the +// failing set EXACTLY matches that file — a regression fails, and so does an +// unexpected improvement (remove the entry to ratchet it in). A floor would let +// a regression hide behind a coincidental improvement. +// +// Raw JSON-RPC over fetch, never the SDK Client — a stock v2 Client negotiates +// the LEGACY era and would report green having tested none of 2026-07-28. +import { spawn } from 'node:child_process' +import { readFileSync } from 'node:fs' +import { setTimeout as sleep } from 'node:timers/promises' +import { fileURLToPath } from 'node:url' + +const DIR = fileURLToPath(new URL('.', import.meta.url)) +const TS_NODE = fileURLToPath(new URL('../../node_modules/.bin/ts-node', import.meta.url)) +const EXPLICIT_PORT = process.env.PORT ? Number(process.env.PORT) : null +const LEVEL = process.env.LEVEL === 'low' ? 'low' : 'high' +const TOKEN = 'acceptance-token' +let BASE + +const GREEN = '\x1b[32m' +const RED = '\x1b[31m' +const DIM = '\x1b[2m' +const RESET = '\x1b[0m' + +const results = [] +let currentEra = '' +function check(name, ok, detail = '') { + results.push({ era: currentEra, name, ok }) + console.log(` ${ok ? GREEN + '✓' : RED + '✗'}${RESET} ${name}${detail ? ` ${DIM}${detail}${RESET}` : ''}`) +} + +async function waitUp() { + // ts-node compiles the Nest graph on boot; be generous. + for (let i = 0; i < 480; i++) { + try { + await fetch(`${BASE}/__events`) + return true + } catch { + await sleep(250) + } + } + return false +} + +const readState = async () => (await fetch(`${BASE}/__events`)).json() +const resetState = () => fetch(`${BASE}/__reset`) + +/** A 2025-era (legacy) request: an initialize handshake, no _meta envelope. */ +function legacyHeaders() { + return { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + authorization: `Bearer ${TOKEN}`, + } +} + +/** + * A 2026-era (modern) request needs ALL of: params._meta carrying + * protocolVersion, clientInfo AND clientCapabilities; an Mcp-Method header; and + * an Mcp-Name header for tools/call. Omitting clientCapabilities returns + * -32602 "Invalid _meta envelope". + */ +function modernHeaders(method, name) { + return { + 'content-type': 'application/json', + accept: 'application/json, text/event-stream', + authorization: `Bearer ${TOKEN}`, + 'mcp-protocol-version': '2026-07-28', + 'mcp-method': method, + ...(name ? { 'mcp-name': name } : {}), + } +} + +function modernMeta() { + return { + 'io.modelcontextprotocol/protocolVersion': '2026-07-28', + 'io.modelcontextprotocol/clientInfo': { name: 'acceptance-client', version: '9.9' }, + 'io.modelcontextprotocol/clientCapabilities': {}, + } +} + +// A real client replays the Mcp-Session-Id it was handed on every later request. +// Without this the harness reports a session/identity failure that no real client +// would see: on a stateless server the replayed token is the ONLY thing carrying +// client info and the negotiated protocol version between per-request instances. +let sessionId + +let id = 0 +async function rpc(era, method, params = {}, name) { + const body = + era === 'modern' + ? { jsonrpc: '2.0', id: ++id, method, params: { ...params, _meta: modernMeta() } } + : { jsonrpc: '2.0', id: ++id, method, params } + const headers = era === 'modern' ? modernHeaders(method, name) : legacyHeaders() + if (sessionId) headers['mcp-session-id'] = sessionId + const res = await fetch(`${BASE}/mcp`, { method: 'POST', headers, body: JSON.stringify(body) }) + // 2026-07-28 removed sessions from the protocol, so a modern response has no + // header to capture — the `if` is what keeps that an absence, not a bug. + const minted = res.headers.get('mcp-session-id') + if (minted) sessionId = minted + const text = await res.text() + let json + try { + json = JSON.parse(text) + } catch { + // SSE framing — pull the data: line out + const line = text.split('\n').find((l) => l.startsWith('data:')) + json = line ? JSON.parse(line.slice(5).trim()) : { raw: text.slice(0, 200) } + } + return { status: res.status, headers: res.headers, json } +} + +async function runEra(era) { + console.log(`\n${era === 'modern' ? '2026-07-28 (modern)' : '2025-11-25 (legacy)'} · LEVEL=${LEVEL}`) + currentEra = era + sessionId = undefined + await resetState() + + if (era === 'legacy') { + await rpc('legacy', 'initialize', { + protocolVersion: '2025-11-25', + capabilities: {}, + clientInfo: { name: 'acceptance-client', version: '9.9' }, + }) + } + + const list = await rpc(era, 'tools/list') + const tools = list.json?.result?.tools ?? [] + const echoTool = tools.find((t) => t.name === 'echo') + + const call = await rpc(era, 'tools/call', { name: 'echo', arguments: { text: 'hi' } }, 'echo') + await rpc(era, 'tools/call', { name: 'fail_always', arguments: {} }, 'fail_always') + + await sleep(150) + const { events, warnings, extraShapes = [] } = await readState() + const toolCalls = events.filter((e) => e.event === '$mcp_tool_call') + const listings = events.filter((e) => e.event === '$mcp_tools_list') + const echoCall = toolCalls.find((e) => e.properties?.$mcp_tool_name === 'echo') + const failCall = toolCalls.find((e) => e.properties?.$mcp_tool_name === 'fail_always') + + // --- the server still works at all (regression floor) --- + check('host server answers tools/list', list.status === 200 && tools.length > 0, `${tools.length} tools`) + check('host server answers tools/call', call.status === 200, `HTTP ${call.status}`) + check('tool result is correct', call.json?.result?.content?.[0]?.text === 'hi') + + // --- Milestone A: it captures at all --- + check('instrument() logged no warning', warnings.length === 0, warnings[0] ?? '') + check('$mcp_tools_list captured', listings.length >= 1, `got ${listings.length}`) + check('$mcp_tool_call captured for echo', !!echoCall) + check('$mcp_tool_call captured for fail_always', !!failCall) + check('error call recorded as an error', failCall?.properties?.$mcp_is_error === true) + check( + 'error message is clean', + failCall?.properties?.$mcp_error_message === 'intentional failure', + JSON.stringify(failCall?.properties?.$mcp_error_message) + ) + + // --- the injected context parameter reaches the wire --- + check('context parameter advertised on tools/list', !!echoTool?.inputSchema?.properties?.context) + check('conversation_id advertised on tools/list', !!echoTool?.inputSchema?.properties?.conversation_id) + + // --- Milestone B: identity --- + check('identify() resolved a user', echoCall?.distinctId === `user_${TOKEN}`, `distinct_id=${echoCall?.distinctId}`) + // ADR-0006: host callbacks get the SDK's own `extra`, unnormalised. A + // synthesised `requestInfo` on v2 would be a convincing partial lie, so its + // absence is the assertion — previously proved only by letting a v1-shaped + // identify() fail, which cost four assertions and asserted nothing directly. + check( + 'extra is not normalised to the v1 shape', + extraShapes.length > 0 && extraShapes.every((s) => !s.hasRequestInfo), + `${extraShapes.length} observed, ${extraShapes.filter((s) => s.hasRequestInfo).length} with requestInfo` + ) + check( + 'getRequestHeaders() resolves headers on v2', + extraShapes.length > 0 && extraShapes.every((s) => s.headersResolved), + `${extraShapes.filter((s) => s.headersResolved).length}/${extraShapes.length}` + ) + check('client name recorded', !!echoCall?.properties?.$mcp_client_name, echoCall?.properties?.$mcp_client_name) + check( + 'protocol version recorded', + !!echoCall?.properties?.$mcp_protocol_version, + echoCall?.properties?.$mcp_protocol_version + ) + + // --- Milestone C: ownership on a per-request instance --- + const intentCall = await rpc( + era, + 'tools/call', + { name: 'echo', arguments: { text: 'hi', context: 'checking adoption numbers' } }, + 'echo' + ) + await sleep(150) + const after = await readState() + const withIntent = after.events.filter((e) => e.event === '$mcp_tool_call').find((e) => e.properties?.$mcp_intent) + check( + '$mcp_intent captured from the context argument', + withIntent?.properties?.$mcp_intent === 'checking adoption numbers', + JSON.stringify(withIntent?.properties?.$mcp_intent) + ) + check( + 'context argument stripped before the tool ran', + intentCall.json?.result?.content?.[0]?.text === 'hi', + JSON.stringify(intentCall.json?.result?.content?.[0]?.text) + ) + + // --- spec: no session header on a modern-era response --- + if (era === 'modern') { + check('no Mcp-Session-Id on a 2026-era response', !call.headers.get('mcp-session-id')) + } +} + +// With an explicit PORT, fail fast rather than silently assert against someone +// else's server — a stale process holding the port may have booted with a +// different LEVEL. (Meaningless for the ephemeral default.) +if (EXPLICIT_PORT) { + try { + await fetch(`http://localhost:${EXPLICIT_PORT}/__events`) + console.error(`port ${EXPLICIT_PORT} is already serving. Kill it first before verifying.`) + process.exit(1) + } catch { + // nothing listening — good + } +} + +// Spawn the binary directly, not through a runner: an extra process layer would +// orphan the ts-node grandchild that actually holds the port when killed. +// `detached` puts the child in its own group so the whole tree dies with it. +const child = spawn(TS_NODE, ['src/main.ts'], { + cwd: DIR, + env: { ...process.env, PORT: String(EXPLICIT_PORT ?? 0), LEVEL }, + stdio: ['ignore', 'pipe', 'inherit'], + detached: true, +}) + +function killServer() { + try { + process.kill(-child.pid, 'SIGKILL') + } catch { + try { + child.kill('SIGKILL') + } catch { + /* already gone */ + } + } +} +process.on('exit', killServer) +for (const sig of ['SIGINT', 'SIGTERM']) { + process.on(sig, () => { + killServer() + process.exit(1) + }) +} + +// The server announces its (possibly ephemeral) port on stdout. +const port = await new Promise((resolve) => { + let out = '' + child.stdout.on('data', (d) => { + process.stdout.write(d) + out += d + const m = out.match(/MCP_HARNESS_LISTENING port=(\d+)/) + if (m) resolve(Number(m[1])) + }) + child.on('exit', () => resolve(null)) + setTimeout(() => resolve(null), 120000) +}) +if (!port) { + console.error('server never announced a port') + killServer() + process.exit(1) +} +BASE = `http://localhost:${port}` + +if (!(await waitUp())) { + console.error('server never came up') + killServer() + process.exit(1) +} + +await runEra('legacy') +await runEra('modern') + +killServer() + +// ── expected-failures reconciliation ──────────────────────────────────────── +const key = (f) => `${f.era} · ${f.name}` +const expected = new Set( + JSON.parse(readFileSync(new URL('./expected-failures.json', import.meta.url), 'utf8')).map(key) +) +const failing = new Set(results.filter((r) => !r.ok).map(key)) +const regressed = [...failing].filter((k) => !expected.has(k)) +const nowPassing = [...expected].filter((k) => !failing.has(k)) + +const passed = results.filter((r) => r.ok).length +console.log(`\n${passed}/${results.length} passed (${expected.size} expected failure(s))`) +if (regressed.length > 0) { + console.error(`${RED}regressed:${RESET} ${regressed.join(' · ')}`) +} +if (nowPassing.length > 0) { + console.error(`${GREEN}now passing — remove from expected-failures.json:${RESET} ${nowPassing.join(' · ')}`) +} +process.exit(regressed.length === 0 && nowPassing.length === 0 ? 0 : 1) diff --git a/packages/mcp/harness/run-all.mjs b/packages/mcp/harness/run-all.mjs new file mode 100644 index 0000000000..70c1ae2867 --- /dev/null +++ b/packages/mcp/harness/run-all.mjs @@ -0,0 +1,49 @@ +// Local runner for the @posthog/mcp integration harness: builds the package, +// then runs the same four lanes CI runs and prints a summary. +// +// pnpm test:mcp-harness (from the repo root) +// +// Individual lanes (what CI runs, one per job): +// pnpm --filter @posthog/mcp run test:integration:sdk-v1 +// pnpm --filter @posthog/mcp run test:integration:sdk-v2 +// pnpm --filter @posthog/mcp run test:integration:nest-v1 +// pnpm --filter @posthog/mcp run test:integration:nest-v2 +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +const PKG_DIR = fileURLToPath(new URL('..', import.meta.url)) // packages/mcp +const ROOT = fileURLToPath(new URL('../../..', import.meta.url)) + +// Resolve the package manager that invoked us, so `node run-all.mjs` also works. +const PM = process.env.npm_execpath +const pm = (args, cwd) => + PM + ? spawnSync(process.execPath, [PM, ...args], { cwd, stdio: 'inherit' }) + : spawnSync('pnpm', [...args], { cwd, stdio: 'inherit' }) + +// Scoped build: @posthog/mcp plus its workspace dependency closure +// (@posthog/types, @posthog/core, posthog-node — needed for its .d.ts build), +// so a package outside that closure cannot take the harness down. +console.log('· building @posthog/mcp (turbo, scoped)') +const build = pm(['exec', 'turbo', 'run', 'build', '--filter=@posthog/mcp'], ROOT) +if (build.status !== 0) process.exit(build.status ?? 1) + +const LANES = [ + ['official SDK v1', 'test:integration:sdk-v1'], + ['official SDK v2', 'test:integration:sdk-v2'], + ['mcp-nest (SDK v1)', 'test:integration:nest-v1'], + ['mcp-nest (SDK v2)', 'test:integration:nest-v2'], +] + +const outcomes = [] +for (const [name, script] of LANES) { + console.log(`\n━━ ${name} ━━`) + const res = pm(['run', script], PKG_DIR) + outcomes.push([name, res.status === 0]) +} + +console.log('\n━━ summary ━━') +for (const [name, ok] of outcomes) { + console.log(` ${ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m'} ${name}`) +} +process.exit(outcomes.every(([, ok]) => ok) ? 0 : 1) diff --git a/packages/mcp/jest.config.mjs b/packages/mcp/jest.config.mjs index 2c628e4025..36d5915c6e 100644 --- a/packages/mcp/jest.config.mjs +++ b/packages/mcp/jest.config.mjs @@ -3,6 +3,7 @@ export default { '^@/(.*)$': '/src/$1', }, modulePathIgnorePatterns: ['/src/__tests__/test-utils/*'], + testPathIgnorePatterns: ['/harness/'], collectCoverage: true, clearMocks: true, fakeTimers: { enableGlobally: false }, diff --git a/packages/mcp/package.json b/packages/mcp/package.json index f2ddf97dd2..c91b07df73 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -12,11 +12,16 @@ }, "scripts": { "clean": "rimraf dist coverage", - "lint": "eslint src", - "lint:fix": "eslint src --fix", + "lint": "eslint src && prettier --check harness", + "lint:fix": "eslint src --fix && prettier --write harness", "generate-version": "node -p \"'export const version = \\'' + require('./package.json').version + '\\''\" > src/version.ts", "pretest:unit": "pnpm generate-version", "test:unit": "jest", + "test:integration": "pnpm test:integration:sdk-v1 && pnpm test:integration:sdk-v2 && pnpm test:integration:nest-v1 && pnpm test:integration:nest-v2", + "test:integration:sdk-v1": "node harness/dual-era/matrix.mjs --major v1 && node harness/dual-era/probe-late-handlers.mjs --major v1 && node harness/dual-era/probe-pagination.mjs --major v1", + "test:integration:sdk-v2": "node harness/dual-era/matrix.mjs --major v2 && node harness/dual-era/probe-late-handlers.mjs --major v2 && node harness/dual-era/probe-first-call-error.mjs && node harness/dual-era/probe-pagination.mjs --major v2", + "test:integration:nest-v1": "node harness/nest-v1/verify.mjs && LEVEL=low node harness/nest-v1/verify.mjs", + "test:integration:nest-v2": "node harness/nest-v2/verify.mjs && LEVEL=low node harness/nest-v2/verify.mjs", "prebuild": "pnpm generate-version", "build": "rslib build", "dev": "rslib build -w", @@ -46,15 +51,27 @@ "devDependencies": { "@babel/preset-env": "catalog:", "@babel/preset-typescript": "catalog:", - "@modelcontextprotocol/sdk": "~1.29.0", - "@modelcontextprotocol/server": "~2.0.0", + "@modelcontextprotocol/node": "^2.0.0", + "@modelcontextprotocol/sdk": "^1.30.0", + "@modelcontextprotocol/server": "^2.0.0", + "@nestjs/common": "^11.1.27", + "@nestjs/core": "^11.1.27", + "@nestjs/microservices": "^11.1.27", + "@nestjs/platform-express": "^11.1.27", "@posthog-tooling/tsconfig-base": "workspace:*", "@rslib/core": "catalog:", "@types/jest": "catalog:", "@types/node": "^20.0.0", "jest": "catalog:", "posthog-node": "workspace:^", - "zod": "^3.25.0" + "reflect-metadata": "^0.2.2", + "rekog-mcp-nest-v1": "npm:@rekog/mcp-nest@1.9.11", + "rekog-mcp-nest-v2": "npm:@rekog/mcp-nest@2.0.0", + "rxjs": "^7.8.2", + "ts-node": "^10.9.2", + "typescript": "catalog:", + "zod": "^3.25.0", + "zod4": "npm:zod@^4.4.3" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7dd45b7b1..2d8c7e91e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,7 +237,7 @@ importers: version: 7.28.5(@babel/core@7.28.5) '@google/genai': specifier: ^1.52.0 - version: 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.1.13)) + version: 1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.1.13)) '@langchain/core': specifier: ^1.2.5 version: 1.2.5(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.49.0(@aws-sdk/credential-provider-node@3.972.30)(@smithy/signature-v4@5.6.12)(ws@8.21.3)(zod@4.1.13))(ws@8.21.3) @@ -615,18 +615,33 @@ importers: '@babel/preset-typescript': specifier: 'catalog:' version: 7.28.5(@babel/core@7.28.5) + '@modelcontextprotocol/node': + specifier: ^2.0.0 + version: 2.0.0(@modelcontextprotocol/server@2.0.0)(hono@4.13.1) '@modelcontextprotocol/sdk': - specifier: ~1.29.0 - version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + specifier: ^1.30.0 + version: 1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) '@modelcontextprotocol/server': - specifier: ~2.0.0 + specifier: ^2.0.0 version: 2.0.0 + '@nestjs/common': + specifier: ^11.1.27 + version: 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.1.27 + version: 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/microservices': + specifier: ^11.1.27 + version: 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(ioredis@5.11.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.1.27 + version: 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29) '@posthog-tooling/tsconfig-base': specifier: workspace:* version: link:../../tooling/tsconfig-base '@rslib/core': specifier: 'catalog:' - version: 0.10.6(@microsoft/api-extractor@7.58.9(@types/node@20.19.9))(typescript@5.9.3) + version: 0.10.6(@microsoft/api-extractor@7.58.9(@types/node@20.19.9))(typescript@5.8.2) '@types/jest': specifier: 'catalog:' version: 29.5.14 @@ -635,13 +650,34 @@ importers: version: 20.19.9 jest: specifier: 'catalog:' - version: 29.7.0(@types/node@20.19.9)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3)) + version: 29.7.0(@types/node@20.19.9)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)) posthog-node: specifier: workspace:^ version: link:../node + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rekog-mcp-nest-v1: + specifier: npm:@rekog/mcp-nest@1.9.11 + version: '@rekog/mcp-nest@1.9.11(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/jwt@11.0.2(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)))(@nestjs/passport@11.0.5(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0))(express@5.2.1)(reflect-metadata@0.2.2)(zod@3.25.76)' + rekog-mcp-nest-v2: + specifier: npm:@rekog/mcp-nest@2.0.0 + version: '@rekog/mcp-nest@2.0.0(@modelcontextprotocol/core@2.0.0)(@modelcontextprotocol/node@2.0.0(@modelcontextprotocol/server@2.0.0)(hono@4.13.1))(@modelcontextprotocol/server@2.0.0)(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/microservices@11.1.29)(express@5.2.1)(reflect-metadata@0.2.2)(zod@3.25.76)' + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.9)(typescript@5.8.2) + typescript: + specifier: 'catalog:' + version: 5.8.2 zod: specifier: ^3.25.0 version: 3.25.76 + zod4: + specifier: npm:zod@^4.4.3 + version: zod@4.4.3 packages/next: dependencies: @@ -819,7 +855,7 @@ importers: version: 9.37.0(jiti@2.6.1) nuxt: specifier: ^4.5.1 - version: 4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.0.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.53.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@types/node@20.19.9)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(@vue/compiler-sfc@3.5.40)(cac@6.7.14)(db0@0.3.4)(esbuild@0.25.12)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.140.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.53.3))(rollup@4.53.3)(srvx@0.11.22)(terser@5.48.0)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))(yaml@2.9.0) + version: 4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.0.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.53.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@types/node@20.19.9)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(@vue/compiler-sfc@3.5.40)(cac@6.7.14)(db0@0.3.4)(esbuild@0.25.12)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.140.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.53.3))(rollup@4.53.3)(srvx@0.11.22)(terser@5.48.0)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))(yaml@2.9.0) packages/openfeature-node-provider: devDependencies: @@ -3301,6 +3337,9 @@ packages: commander: optional: true + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} @@ -4998,6 +5037,10 @@ packages: resolution: {integrity: sha512-4lXj3fTPQYGdEtOG9gWDnvmp6wpXNMo9MmWzfZxxPUxMcjulZJa93pYAZ90luFLg2YVdVuUl2tuwdD7tY5K9MA==} engines: {node: '>=20'} + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -5039,6 +5082,16 @@ packages: resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==} engines: {node: '>=20'} + '@modelcontextprotocol/node@2.0.0': + resolution: {integrity: sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==} + engines: {node: '>=20'} + peerDependencies: + '@modelcontextprotocol/server': ^2.0.0 + hono: ^4.11.4 + peerDependenciesMeta: + hono: + optional: true + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -5049,6 +5102,16 @@ packages: '@cfworker/json-schema': optional: true + '@modelcontextprotocol/sdk@1.30.0': + resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@modelcontextprotocol/server@2.0.0': resolution: {integrity: sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==} engines: {node: '>=20'} @@ -5096,6 +5159,96 @@ packages: '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + '@nestjs/common@11.1.29': + resolution: {integrity: sha512-zkeNRlfiQIH/044r5zphjNzKYxBRC4O00onTdCsH2qq0R30Ixo0gOS9so5TadEbJJCuGNm0Vx3PBE2FG2vkBdA==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/config@4.0.4': + resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + + '@nestjs/core@11.1.29': + resolution: {integrity: sha512-ANXnZxirMNAY+JuRHCIYIdTfbhX3rizeSbJloub/4EYwpIfCuUoaz9woOofWmywYYc3gsVkLxVZV03gqntI8vA==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/jwt@11.0.2': + resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + + '@nestjs/microservices@11.1.29': + resolution: {integrity: sha512-Z97VBDwgarzj08ZoC5ItJDIkUiDsAXm3kx2lWc0YtpnOCtQxmGe6tM2eI1tlKJWXDIWBi6iNxtOCJU76F+Y/cA==} + peerDependencies: + '@grpc/grpc-js': '*' + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + amqp-connection-manager: '*' + amqplib: '*' + cache-manager: '*' + ioredis: '*' + kafkajs: '*' + mqtt: '*' + nats: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@nestjs/websockets': + optional: true + amqp-connection-manager: + optional: true + amqplib: + optional: true + cache-manager: + optional: true + ioredis: + optional: true + kafkajs: + optional: true + mqtt: + optional: true + nats: + optional: true + + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + + '@nestjs/platform-express@11.1.29': + resolution: {integrity: sha512-OngsAjdzM+SbbmP1XojKfBgl0Lj7u8b7IK5duwpFymzNP5EmjS8ujvqck9M8h0L14I91aRIFSjIpVfHj8WVtqw==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@next/env@15.5.22': resolution: {integrity: sha512-O5BlKb3KtsHkvO0gjjV66PuJnAgCtIEIzwkt50HRAHsQkU1t77eksIXSZV84/WMtZJjWrnDUPKHVRi0D62nSAA==} @@ -5940,6 +6093,45 @@ packages: resolution: {integrity: sha512-0N202XAqsU/FlE53Nmh6GHyMtGm7g6TeC93mrFAFJOqGRKznT0/ail+cYlU6tNcPA9AHzZu1Modw1eoDINSliQ==} deprecated: This version is no longer supported + '@rekog/mcp-nest@1.9.11': + resolution: {integrity: sha512-r6Qcds0pIqn9fGha/UwrVqJxr7BHovl6jZKtW6NEhxeM+W3AxkAvvrIknjO2PgPk7Sq4qVHd+nlzU37ZmLhbtg==} + peerDependencies: + '@modelcontextprotocol/sdk': '>=1.10.0' + '@nestjs/common': '>=9.0.0' + '@nestjs/core': '>=9.0.0' + '@nestjs/jwt': ^11.0.0 + '@nestjs/passport': ^11.0.5 + '@nestjs/platform-fastify': ^11.1.5 + '@nestjs/typeorm': '>=9.0.0' + express: '>=4.0.0' + reflect-metadata: ^0.2.2 + typeorm: '>=0.3.25' + zod: ^4.3.5 + peerDependenciesMeta: + '@nestjs/platform-fastify': + optional: true + '@nestjs/typeorm': + optional: true + typeorm: + optional: true + + '@rekog/mcp-nest@2.0.0': + resolution: {integrity: sha512-lT5V26V4fvPay50zMiw2KrEqlDSuzdAeyoQFd0+vfAwb40G6H8p8psTzgq5atTgNHktRPI7u4x8lrm8X844Asg==} + peerDependencies: + '@modelcontextprotocol/core': ^2.0.0-beta.5 + '@modelcontextprotocol/node': ^2.0.0-beta.5 + '@modelcontextprotocol/server': ^2.0.0-beta.5 + '@nestjs/common': '>=9.0.0' + '@nestjs/core': '>=9.0.0' + '@nestjs/microservices': '>=9.0.0' + '@nestjs/platform-fastify': ^11.1.5 + express: '>=4.0.0' + reflect-metadata: ^0.2.2 + zod: ^4.3.5 + peerDependenciesMeta: + '@nestjs/platform-fastify': + optional: true + '@rolldown/binding-android-arm64@1.2.1': resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7013,6 +7205,13 @@ packages: '@types/react-dom': optional: true + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tootallnate/once@1.1.2': resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} @@ -7198,6 +7397,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/lodash@4.17.21': resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} @@ -8146,6 +8348,9 @@ packages: appdirsjs@1.2.7: resolution: {integrity: sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==} + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + archiver-utils@5.0.2: resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} engines: {node: '>= 14'} @@ -8497,6 +8702,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + base@0.11.2: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} engines: {node: '>=0.10.0'} @@ -8686,6 +8895,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.0: resolution: {integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==} engines: {node: '>= 0.8'} @@ -9113,6 +9326,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + concat-with-sourcemaps@1.1.0: resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} @@ -9191,6 +9408,13 @@ packages: cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -9868,10 +10092,18 @@ packages: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -10564,6 +10796,9 @@ packages: resolution: {integrity: sha512-tWhw7z4jFuQgZB9tbQyUh5BY9nNd/wimM+fBLfmmJjakkJDNvbJKm0nQ5ruPKC0us1HGg7L6iBk1fxpSzcgSaA==} hasBin: true + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} @@ -10653,6 +10888,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -11549,10 +11788,6 @@ packages: resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} - ip-address@10.3.1: - resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} - engines: {node: '>= 12'} - ip-address@10.5.0: resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} engines: {node: '>= 12'} @@ -12006,6 +12241,10 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -12432,6 +12671,10 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -12714,6 +12957,10 @@ packages: enquirer: optional: true + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + loader-runner@4.3.2: resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} engines: {node: '>=6.11.5'} @@ -12760,12 +13007,33 @@ packages: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -12778,6 +13046,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@2.2.0: resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} engines: {node: '>=4'} @@ -13428,6 +13699,10 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + multer@2.2.0: + resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} + engines: {node: '>= 10.16.0'} + mustache@2.3.2: resolution: {integrity: sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==} engines: {npm: '>=1.4.0'} @@ -13706,6 +13981,12 @@ packages: engines: {node: '>=18'} hasBin: true + oauth@0.10.2: + resolution: {integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==} + + oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + ob1@0.70.4: resolution: {integrity: sha512-u7UUis2Scwy+RDdJ0T49Urb0yTQTyEYt37lHzWDqpLQSLYZZGT3ZNtCvB88Z9yKhhouKD4TNOGkBJgkFJ+84sg==} @@ -14052,6 +14333,37 @@ packages: resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} engines: {node: '>=0.10.0'} + passport-azure-ad-oauth2@0.0.4: + resolution: {integrity: sha512-yjwi0qXzGPIrR8yI5mBql2wO6tf/G5+HAFllkwwZ6f2EBCVvRv5z+6CwQeBvlrDbFh8RCXdj/IfB17r8LYDQQQ==} + + passport-github@1.1.0: + resolution: {integrity: sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==} + engines: {node: '>= 0.4.0'} + + passport-google-oauth20@2.0.0: + resolution: {integrity: sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==} + engines: {node: '>= 0.4.0'} + + passport-oauth1@1.3.0: + resolution: {integrity: sha512-8T/nX4gwKTw0PjxP1xfD0QhrydQNakzeOpZ6M5Uqdgz9/a/Ag62RmJxnZQ4LkbdXGrRehQHIAHNAu11rCP46Sw==} + engines: {node: '>= 0.4.0'} + + passport-oauth2@1.8.0: + resolution: {integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==} + engines: {node: '>= 0.4.0'} + + passport-oauth@1.0.0: + resolution: {integrity: sha512-4IZNVsZbN1dkBzmEbBqUxDG8oFOIK81jqdksE3HEb/vI3ib3FMjbiZZ6MTtooyYZzmKu0BfovjvT1pdGgIq+4Q==} + engines: {node: '>= 0.4.0'} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -14125,6 +14437,9 @@ packages: pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -15349,6 +15664,9 @@ packages: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -16221,6 +16539,10 @@ packages: stream-combiner@0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + streamx@2.23.0: resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} @@ -16365,6 +16687,10 @@ packages: strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + structured-clone-es@2.0.1: resolution: {integrity: sha512-10ZL5r77LhknxlP1FBiCW+VdnuWOEFLdSS2SKtjyEV+L4qP1hUEIMIZW94LC3jKxRmT/Dj7KkD1mqh/IY6lhKQ==} @@ -16699,6 +17025,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -16928,6 +17258,9 @@ packages: resolution: {integrity: sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==} engines: {node: '>= 0.4'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@4.7.4: resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} engines: {node: '>=4.2.0'} @@ -16968,6 +17301,17 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + ultrahtml@1.6.0: resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} @@ -21000,6 +21344,8 @@ snapshots: cac: 6.7.14 citty: 0.2.2 + '@borewit/text-codec@0.2.2': {} + '@cfworker/json-schema@4.1.1': {} '@changesets/apply-release-plan@7.0.14': @@ -21234,25 +21580,25 @@ snapshots: dependencies: stackframe: 1.3.4 - '@devframes/hub@0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2))': + '@devframes/hub@0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2))': dependencies: birpc: 4.0.0 destr: 2.0.5 - devframe: 0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2) + devframe: 0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2) nostics: 1.2.0 pathe: 2.0.3 perfect-debounce: 2.1.0 tinyexec: 1.2.4 zigpty: 0.2.1 - '@devframes/json-render@0.7.15(@devframes/hub@0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)))(devframe@0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2))': + '@devframes/json-render@0.7.15(@devframes/hub@0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)))(devframe@0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2))': dependencies: '@json-render/core': 0.19.0(zod@4.4.3) - devframe: 0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2) + devframe: 0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2) nostics: 1.2.0 zod: 4.4.3 optionalDependencies: - '@devframes/hub': 0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)) + '@devframes/hub': 0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)) '@dxup/nuxt@0.5.5(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(magicast@0.5.3)(oxc-parser@0.140.0)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: @@ -22321,14 +22667,14 @@ snapshots: '@gar/promisify@1.1.3': {} - '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.1.13))': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.1.13))': dependencies: google-auth-library: 10.9.0 p-retry: 4.6.2 protobufjs: 7.6.5 ws: 8.21.1 optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.1.13) + '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(zod@4.1.13) transitivePeerDependencies: - bufferutil - supports-color @@ -22517,6 +22863,43 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 + '@jest/core@29.7.0(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0(node-notifier@8.0.2) + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.20.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + optionalDependencies: + node-notifier: 8.0.2 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 @@ -23068,6 +23451,8 @@ snapshots: - openai - ws + '@lukeed/csprng@1.1.0': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.6 @@ -23250,7 +23635,39 @@ snapshots: dependencies: zod: 4.4.3 - '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': + '@modelcontextprotocol/node@2.0.0(@modelcontextprotocol/server@2.0.0)(hono@4.13.1)': + dependencies: + '@hono/node-server': 1.19.17(hono@4.13.1) + '@modelcontextprotocol/server': 2.0.0 + optionalDependencies: + hono: 4.13.1 + + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.1.13)': + dependencies: + '@hono/node-server': 1.19.17(hono@4.13.1) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.6.2(express@5.2.1) + hono: 4.13.1 + jose: 6.2.8 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.1.13 + zod-to-json-schema: 3.25.2(zod@4.1.13) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.17(hono@4.13.1) ajv: 8.20.0 @@ -23274,7 +23691,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.1.13)': + '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.1.13)': dependencies: '@hono/node-server': 1.19.17(hono@4.13.1) ajv: 8.20.0 @@ -23299,7 +23716,7 @@ snapshots: - supports-color optional: true - '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)': + '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.17(hono@4.13.1) ajv: 8.20.0 @@ -23393,6 +23810,74 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + transitivePeerDependencies: + - supports-color + + '@nestjs/config@4.0.4(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 + dotenv-expand: 12.0.3 + lodash: 4.18.1 + rxjs: 7.8.2 + + '@nestjs/core@11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/microservices': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(ioredis@5.11.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29) + + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 + + '@nestjs/microservices@11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(ioredis@5.11.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2) + iterare: 1.2.1 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + ioredis: 5.11.1 + + '@nestjs/passport@11.0.5(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + passport: 0.7.0 + + '@nestjs/platform-express@11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)': + dependencies: + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.2.0 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@next/env@15.5.22': {} '@next/swc-darwin-arm64@15.5.22': @@ -23732,11 +24217,11 @@ snapshots: - vue - vue-tsc - '@nuxt/nitro-server@4.5.1(f06784ba7edfe4afea81417752d241bc)': + '@nuxt/nitro-server@4.5.1(5b245cdf7403554dfa5aca9a4e5f08e4)': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.3)(oxc-parser@0.140.0)(rolldown@1.2.1)(unplugin@3.3.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))) - '@unhead/vue': 3.2.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.12)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) + '@unhead/vue': 3.2.3(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.12)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) '@vue/shared': 3.5.40 consola: 3.4.2 defu: 6.1.7 @@ -23751,7 +24236,7 @@ snapshots: mocked-exports: 0.1.1 nitropack: 2.13.4(@parcel/watcher@2.5.6)(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(oxc-parser@0.140.0)(rolldown@1.2.1)(srvx@0.11.22)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) nostics: 1.2.0 - nuxt: 4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.0.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.53.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@types/node@20.19.9)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(@vue/compiler-sfc@3.5.40)(cac@6.7.14)(db0@0.3.4)(esbuild@0.25.12)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.140.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.53.3))(rollup@4.53.3)(srvx@0.11.22)(terser@5.48.0)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))(yaml@2.9.0) + nuxt: 4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.0.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.53.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@types/node@20.19.9)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(@vue/compiler-sfc@3.5.40)(cac@6.7.14)(db0@0.3.4)(esbuild@0.25.12)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.140.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.53.3))(rollup@4.53.3)(srvx@0.11.22)(terser@5.48.0)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))(yaml@2.9.0) nypm: 0.6.8 ohash: 2.0.11 pathe: 2.0.3 @@ -23846,7 +24331,7 @@ snapshots: rc9: 3.0.1 std-env: 4.2.0 - '@nuxt/vite-builder@4.5.1(ed56ee776a415eac9cb5d0a9d0e918a0)': + '@nuxt/vite-builder@4.5.1(96daf905def999414fe6c216af7764e1)': dependencies: '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.3)(oxc-parser@0.140.0)(rolldown@1.2.1)(unplugin@3.3.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))) '@vitejs/plugin-vue': 6.0.8(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2)) @@ -23864,7 +24349,7 @@ snapshots: knitwork: 1.3.0 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.0.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.53.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@types/node@20.19.9)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(@vue/compiler-sfc@3.5.40)(cac@6.7.14)(db0@0.3.4)(esbuild@0.25.12)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.140.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.53.3))(rollup@4.53.3)(srvx@0.11.22)(terser@5.48.0)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))(yaml@2.9.0) + nuxt: 4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.0.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.53.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@types/node@20.19.9)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(@vue/compiler-sfc@3.5.40)(cac@6.7.14)(db0@0.3.4)(esbuild@0.25.12)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.140.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.53.3))(rollup@4.53.3)(srvx@0.11.22)(terser@5.48.0)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))(yaml@2.9.0) nypm: 0.6.8 pathe: 2.0.3 pkg-types: 2.3.1 @@ -24963,6 +25448,41 @@ snapshots: dependencies: nanoid: 3.3.16 + '@rekog/mcp-nest@1.9.11(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/jwt@11.0.2(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)))(@nestjs/passport@11.0.5(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0))(express@5.2.1)(reflect-metadata@0.2.2)(zod@3.25.76)': + dependencies: + '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': 4.0.4(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': 11.0.2(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/passport': 11.0.5(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + cookie-parser: 1.4.7 + express: 5.2.1 + multer: 2.2.0 + passport: 0.7.0 + passport-azure-ad-oauth2: 0.0.4 + passport-github: 1.1.0 + passport-google-oauth20: 2.0.0 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + zod: 3.25.76 + + '@rekog/mcp-nest@2.0.0(@modelcontextprotocol/core@2.0.0)(@modelcontextprotocol/node@2.0.0(@modelcontextprotocol/server@2.0.0)(hono@4.13.1))(@modelcontextprotocol/server@2.0.0)(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/microservices@11.1.29)(express@5.2.1)(reflect-metadata@0.2.2)(zod@3.25.76)': + dependencies: + '@modelcontextprotocol/core': 2.0.0 + '@modelcontextprotocol/node': 2.0.0(@modelcontextprotocol/server@2.0.0)(hono@4.13.1) + '@modelcontextprotocol/server': 2.0.0 + '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.29)(ioredis@5.11.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + express: 5.2.1 + multer: 2.2.0 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + zod: 3.25.76 + '@rolldown/binding-android-arm64@1.2.1': optional: true @@ -25379,6 +25899,15 @@ snapshots: core-js: 3.44.0 jiti: 2.7.0 + '@rslib/core@0.10.6(@microsoft/api-extractor@7.58.9(@types/node@20.19.9))(typescript@5.8.2)': + dependencies: + '@rsbuild/core': 1.4.8 + rsbuild-plugin-dts: 0.10.6(@microsoft/api-extractor@7.58.9(@types/node@20.19.9))(@rsbuild/core@1.4.8)(typescript@5.8.2) + tinyglobby: 0.2.17 + optionalDependencies: + '@microsoft/api-extractor': 7.58.9(@types/node@20.19.9) + typescript: 5.8.2 + '@rslib/core@0.10.6(@microsoft/api-extractor@7.58.9(@types/node@20.19.9))(typescript@5.9.3)': dependencies: '@rsbuild/core': 1.4.8 @@ -26164,6 +26693,15 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + '@tootallnate/once@1.1.2': {} '@tootallnate/once@2.0.1': {} @@ -26357,6 +26895,11 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 0.7.31 + '@types/node': 22.20.1 + '@types/lodash@4.17.21': {} '@types/minimatch@3.0.3': {} @@ -26802,9 +27345,9 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@unhead/bundler@3.2.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.10)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(unhead@3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': + '@unhead/bundler@3.2.3(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.10)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(unhead@3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: - '@vitejs/devtools-kit': 0.4.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitejs/devtools-kit': 0.4.10(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) magic-string: 1.1.0 oxc-parser: 0.140.0 oxc-walker: 1.0.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(oxc-parser@0.140.0)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) @@ -26828,9 +27371,9 @@ snapshots: - typescript - unloader - '@unhead/bundler@3.2.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.12)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(unhead@3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': + '@unhead/bundler@3.2.3(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.12)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(unhead@3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: - '@vitejs/devtools-kit': 0.4.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitejs/devtools-kit': 0.4.10(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) magic-string: 1.1.0 oxc-parser: 0.140.0 oxc-walker: 1.0.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(oxc-parser@0.140.0)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) @@ -26854,9 +27397,9 @@ snapshots: - typescript - unloader - '@unhead/vue@3.2.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.10)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': + '@unhead/vue@3.2.3(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.10)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: - '@unhead/bundler': 3.2.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.10)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(unhead@3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) + '@unhead/bundler': 3.2.3(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.10)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(unhead@3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) hookable: 6.1.1 unhead: 3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) unplugin: 3.3.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) @@ -26879,9 +27422,9 @@ snapshots: - typescript - unloader - '@unhead/vue@3.2.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.12)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': + '@unhead/vue@3.2.3(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.12)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))': dependencies: - '@unhead/bundler': 3.2.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.12)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(unhead@3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) + '@unhead/bundler': 3.2.3(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.12)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(unhead@3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) hookable: 6.1.1 unhead: 3.2.3(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) unplugin: 3.3.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.12)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) @@ -27013,11 +27556,11 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vitejs/devtools-kit@0.4.10(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitejs/devtools-kit@0.4.10(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: - '@devframes/hub': 0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)) - '@devframes/json-render': 0.7.15(@devframes/hub@0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)))(devframe@0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)) - devframe: 0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2) + '@devframes/hub': 0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)) + '@devframes/json-render': 0.7.15(@devframes/hub@0.7.15(devframe@0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)))(devframe@0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2)) + devframe: 0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2) local-pkg: 1.2.1 mlly: 1.8.2 nostics: 1.2.0 @@ -27131,7 +27674,7 @@ snapshots: '@vue/compiler-core@3.5.22': dependencies: - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@vue/shared': 3.5.22 entities: 4.5.0 estree-walker: 2.0.2 @@ -27529,6 +28072,8 @@ snapshots: appdirsjs@1.2.7: {} + append-field@1.0.0: {} + archiver-utils@5.0.2: dependencies: glob: 10.5.0 @@ -27791,8 +28336,8 @@ snapshots: babel-plugin-jest-hoist@26.6.2: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 '@types/babel__core': 7.1.18 '@types/babel__traverse': 7.14.2 @@ -28084,6 +28629,8 @@ snapshots: base64-js@1.5.1: {} + base64url@3.0.1: {} + base@0.11.2: dependencies: cache-base: 1.0.1 @@ -28325,6 +28872,10 @@ snapshots: dependencies: run-applescript: 7.1.0 + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.0: {} bytes@3.1.2: {} @@ -28817,6 +29368,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + concat-with-sourcemaps@1.1.0: dependencies: source-map: 0.6.1 @@ -28875,6 +29433,13 @@ snapshots: cookie-es@3.1.1: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + cookie-signature@1.0.7: {} cookie-signature@1.2.2: {} @@ -28953,6 +29518,21 @@ snapshots: - ts-node optional: true + create-jest@29.7.0(@types/node@20.19.9)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.9)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@20.19.9)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3)): dependencies: '@jest/types': 29.6.3 @@ -29495,7 +30075,7 @@ snapshots: object-keys: 1.1.1 object.assign: 4.1.7 regexp.prototype.flags: 1.5.4 - side-channel: 1.1.0 + side-channel: 1.1.1 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 which-typed-array: 1.1.19 @@ -29649,7 +30229,7 @@ snapshots: devalue@5.8.2: {} - devframe@0.7.15(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2): + devframe@0.7.15(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(cac@6.7.14)(srvx@0.11.22)(typescript@5.8.2): dependencies: '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@5.8.2)) birpc: 4.0.0 @@ -29662,7 +30242,7 @@ snapshots: ufo: 1.6.4 valibot: 1.4.2(typescript@5.8.2) optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) cac: 6.7.14 transitivePeerDependencies: - srvx @@ -29752,8 +30332,14 @@ snapshots: dependencies: dotenv: 16.4.7 + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.4.7 + dotenv@16.4.7: {} + dotenv@17.4.1: {} + dotenv@17.4.2: {} dotenv@8.6.0: {} @@ -30907,6 +31493,8 @@ snapshots: fast-npm-meta@1.5.1: {} + fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} fast-string-truncated-width@3.0.3: {} @@ -31010,6 +31598,15 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + file-uri-to-path@1.0.0: {} filename-reserved-regex@2.0.0: {} @@ -32055,8 +32652,6 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.3.1: {} - ip-address@10.5.0: {} ip-regex@2.1.0: {} @@ -32453,6 +33048,8 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + iterare@1.2.1: {} + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -32522,6 +33119,27 @@ snapshots: - ts-node optional: true + jest-cli@29.7.0(@types/node@20.19.9)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)): + dependencies: + '@jest/core': 29.7.0(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.19.9)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.9)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.3 + optionalDependencies: + node-notifier: 8.0.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@20.19.9)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3)) @@ -32723,6 +33341,37 @@ snapshots: - supports-color optional: true + jest-config@29.7.0(@types/node@20.19.9)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)): + dependencies: + '@babel/core': 7.29.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.7) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.9 + ts-node: 10.9.2(@types/node@20.19.9)(typescript@5.8.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@20.19.9)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.7 @@ -32817,6 +33466,37 @@ snapshots: - supports-color optional: true + jest-config@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)): + dependencies: + '@babel/core': 7.29.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.7) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.20.1 + ts-node: 10.9.2(@types/node@20.19.9)(typescript@5.8.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.7 @@ -33610,6 +34290,20 @@ snapshots: - ts-node optional: true + jest@29.7.0(@types/node@20.19.9)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)): + dependencies: + '@jest/core': 29.7.0(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.9)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2)) + optionalDependencies: + node-notifier: 8.0.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@20.19.9)(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(node-notifier@8.0.2)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3)) @@ -34010,6 +34704,19 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -34263,6 +34970,8 @@ snapshots: optionalDependencies: enquirer: 2.4.1 + load-esm@1.0.3: {} + loader-runner@4.3.2: {} loader-utils@3.3.1: {} @@ -34305,10 +35014,24 @@ snapshots: lodash.get@4.4.2: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.startcase@4.4.0: {} lodash.throttle@4.1.1: {} @@ -34317,6 +35040,8 @@ snapshots: lodash@4.17.21: {} + lodash@4.18.1: {} + log-symbols@2.2.0: dependencies: chalk: 2.4.2 @@ -34953,8 +35678,8 @@ snapshots: metro-transform-plugins@0.80.12: dependencies: '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/template': 7.27.2 + '@babel/generator': 7.29.8 + '@babel/template': 7.29.7 '@babel/traverse': 7.28.5 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 @@ -34996,9 +35721,9 @@ snapshots: metro-transform-worker@0.80.12: dependencies: '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 flow-enums-runtime: 0.0.6 metro: 0.80.12 metro-babel-transformer: 0.80.12 @@ -35037,11 +35762,11 @@ snapshots: dependencies: '@babel/code-frame': 7.29.7 '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.27.2 + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 '@babel/traverse': 7.28.5 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 absolute-path: 0.0.0 accepts: 1.3.8 async: 3.2.6 @@ -35431,6 +36156,13 @@ snapshots: muggle-string@0.4.1: {} + multer@2.2.0: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + mustache@2.3.2: {} mustache@4.2.0: {} @@ -35748,17 +36480,17 @@ snapshots: nullthrows@1.1.1: {} - nuxt@4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.0.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.53.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@types/node@20.19.9)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(@vue/compiler-sfc@3.5.40)(cac@6.7.14)(db0@0.3.4)(esbuild@0.25.12)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.140.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.53.3))(rollup@4.53.3)(srvx@0.11.22)(terser@5.48.0)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))(yaml@2.9.0): + nuxt@4.5.1(@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.7))(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.0.4(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.53.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(@types/node@20.19.9)(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(@vue/compiler-sfc@3.5.40)(cac@6.7.14)(db0@0.3.4)(esbuild@0.25.12)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.11.1)(lightningcss@1.33.0)(magicast@0.5.3)(optionator@0.9.4)(oxc-parser@0.140.0)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.1)(rollup@4.53.3))(rollup@4.53.3)(srvx@0.11.22)(terser@5.48.0)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))(yaml@2.9.0): dependencies: '@dxup/nuxt': 0.5.5(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(magicast@0.5.3)(oxc-parser@0.140.0)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) '@nuxt/cli': 3.37.0(@nuxt/schema@4.5.1)(@parcel/watcher@2.5.6)(cac@6.7.14)(magicast@0.5.3) '@nuxt/devtools': 3.4.0(@vercel/functions@3.4.3(@aws-sdk/credential-provider-web-identity@3.972.73))(db0@0.3.4)(ioredis@5.11.1)(magic-string@1.1.0)(oxc-parser@0.140.0)(rolldown@1.2.1)(unplugin@3.3.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2)) '@nuxt/kit': 4.5.1(magic-string@1.1.0)(magicast@0.5.3)(oxc-parser@0.140.0)(rolldown@1.2.1)(unplugin@3.3.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23))) - '@nuxt/nitro-server': 4.5.1(f06784ba7edfe4afea81417752d241bc) + '@nuxt/nitro-server': 4.5.1(5b245cdf7403554dfa5aca9a4e5f08e4) '@nuxt/schema': 4.5.1 '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.5.1(magic-string@1.1.0)(magicast@0.5.3)(oxc-parser@0.140.0)(rolldown@1.2.1)(unplugin@3.3.0(@rspack/core@1.4.8(@swc/helpers@0.5.17))(esbuild@0.25.10)(rolldown@1.2.1)(rollup@4.53.3)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)))) - '@nuxt/vite-builder': 4.5.1(ed56ee776a415eac9cb5d0a9d0e918a0) - '@unhead/vue': 3.2.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.10)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) + '@nuxt/vite-builder': 4.5.1(96daf905def999414fe6c216af7764e1) + '@unhead/vue': 3.2.3(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@rspack/core@1.4.8(@swc/helpers@0.5.17))(cac@6.7.14)(esbuild@0.25.10)(lightningcss@1.33.0)(rolldown@1.2.1)(rollup@4.53.3)(srvx@0.11.22)(typescript@5.8.2)(vite@8.2.0(@types/node@20.19.9)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.40(typescript@5.8.2))(webpack@5.108.1(esbuild@0.25.12)(lightningcss@1.33.0)(postcss@8.5.23)) '@vue/shared': 3.5.40 chokidar: 5.0.0 compatx: 0.2.0 @@ -35904,6 +36636,10 @@ snapshots: pathe: 2.0.3 tinyexec: 1.3.0 + oauth@0.10.2: {} + + oauth@0.9.15: {} + ob1@0.70.4: {} ob1@0.80.12: @@ -36327,6 +37063,45 @@ snapshots: pascalcase@0.1.1: {} + passport-azure-ad-oauth2@0.0.4: + dependencies: + passport-oauth: 1.0.0 + + passport-github@1.1.0: + dependencies: + passport-oauth2: 1.8.0 + + passport-google-oauth20@2.0.0: + dependencies: + passport-oauth2: 1.8.0 + + passport-oauth1@1.3.0: + dependencies: + oauth: 0.9.15 + passport-strategy: 1.0.0 + utils-merge: 1.0.1 + + passport-oauth2@1.8.0: + dependencies: + base64url: 3.0.1 + oauth: 0.10.2 + passport-strategy: 1.0.0 + uid2: 0.0.4 + utils-merge: 1.0.1 + + passport-oauth@1.0.0: + dependencies: + passport-oauth1: 1.3.0 + passport-oauth2: 1.8.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + path-browserify@1.0.1: {} path-exists@3.0.0: {} @@ -36379,6 +37154,8 @@ snapshots: dependencies: through: 2.3.8 + pause@0.0.1: {} + pend@1.2.0: {} perfect-debounce@2.1.0: {} @@ -37547,10 +38324,8 @@ snapshots: which: 2.0.2 yargs: 17.7.2 transitivePeerDependencies: - - bufferutil - supports-color - typescript - - utf-8-validate react-native-codegen@0.69.2(@babel/preset-env@7.28.5(@babel/core@7.28.5)): dependencies: @@ -37828,6 +38603,8 @@ snapshots: dependencies: '@eslint-community/regexpp': 4.12.2 + reflect-metadata@0.2.2: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -38236,6 +39013,18 @@ snapshots: rrweb-cssom@0.6.0: {} + rsbuild-plugin-dts@0.10.6(@microsoft/api-extractor@7.58.9(@types/node@20.19.9))(@rsbuild/core@1.4.8)(typescript@5.8.2): + dependencies: + '@ast-grep/napi': 0.37.0 + '@rsbuild/core': 1.4.8 + magic-string: 0.30.21 + picocolors: 1.1.1 + tinyglobby: 0.2.17 + tsconfig-paths: 4.2.0 + optionalDependencies: + '@microsoft/api-extractor': 7.58.9(@types/node@20.19.9) + typescript: 5.8.2 + rsbuild-plugin-dts@0.10.6(@microsoft/api-extractor@7.58.9(@types/node@20.19.9))(@rsbuild/core@1.4.8)(typescript@5.9.3): dependencies: '@ast-grep/napi': 0.37.0 @@ -38743,7 +39532,7 @@ snapshots: socks@2.8.7: dependencies: - ip-address: 10.3.1 + ip-address: 10.5.0 smart-buffer: 4.2.0 source-map-js@1.2.1: {} @@ -38887,6 +39676,8 @@ snapshots: dependencies: duplexer: 0.1.2 + streamsearch@1.1.0: {} + streamx@2.23.0: dependencies: events-universal: 1.0.1 @@ -39051,6 +39842,10 @@ snapshots: strnum@2.2.3: {} + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + structured-clone-es@2.0.1: {} structured-headers@0.4.1: {} @@ -39605,6 +40400,12 @@ snapshots: toidentifier@1.0.1: {} + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + totalist@3.0.1: {} tough-cookie@4.1.3: @@ -39749,6 +40550,24 @@ snapshots: esbuild: 0.25.10 jest-util: 29.7.0 + ts-node@10.9.2(@types/node@20.19.9)(typescript@5.8.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.9 + acorn: 8.18.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.8.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -40034,6 +40853,8 @@ snapshots: typed-array-buffer: 1.0.3 typed-array-byte-offset: 1.0.4 + typedarray@0.0.6: {} + typescript@4.7.4: {} typescript@5.8.2: {} @@ -40056,6 +40877,14 @@ snapshots: uglify-js@3.19.3: optional: true + uid2@0.0.4: {} + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + ultrahtml@1.6.0: {} ultrahtml@1.7.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 921d6886f2..124e02ef28 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -101,6 +101,16 @@ trustPolicyExclude: trustPolicy: no-downgrade +peerDependencyRules: + allowedVersions: + # @rekog/mcp-nest (used by the packages/mcp integration harness via the + # rekog-mcp-nest-v1/v2 aliases) peers on zod ^4, while packages/mcp provides + # zod 3 for the v1 MCP SDK. The mismatch is harmless: the harness fixtures + # pass zod-4 schemas from their own zod4 alias (nest-v1) or raw JSON Schema + # objects (nest-v2), so the code paths that would need rekog's own zod link + # to be v4 are never reached. + '@rekog/mcp-nest>zod': '3 || 4' + patchedDependencies: browserstack-local@1.5.1: patches/browserstack-local@1.5.1.patch tr46@0.0.3: patches/tr46@0.0.3.patch