diff --git a/README.md b/README.md index 96a7abc..10737ed 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This repository provides a multi-language reference implementation of the propos | C# | `csharp/sdk/` | `ModelContextProtocol.Interceptors` | In Progress | | Go | `go/sdk/` | `github.com/modelcontextprotocol/ext-interceptors/go/sdk` | Planned | | Python | `python/sdk/` | `mcp-ext-interceptors` | Planned | -| TypeScript | `typescript/sdk/` | `@ext-modelcontextprotocol/interceptors` | Planned | +| TypeScript | `typescript/sdk/` | `mcp-ext-interceptors` | In progress | ## CI/CD diff --git a/typescript/sdk/.eslintrc.json b/typescript/sdk/.eslintrc.json index db42afc..83e33bd 100644 --- a/typescript/sdk/.eslintrc.json +++ b/typescript/sdk/.eslintrc.json @@ -4,7 +4,7 @@ "parserOptions": { "ecmaVersion": "latest", "sourceType": "module", - "project": "./tsconfig.json" + "project": "./tsconfig.eslint.json" }, "plugins": ["@typescript-eslint"], "extends": [ diff --git a/typescript/sdk/README.md b/typescript/sdk/README.md index 37523a0..ad704f0 100644 --- a/typescript/sdk/README.md +++ b/typescript/sdk/README.md @@ -1,40 +1,179 @@ # MCP Interceptors TypeScript SDK -TypeScript implementation of the Model Context Protocol (MCP) interceptor framework. +TypeScript implementation of [SEP-1763](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763) — gateway-level interceptors for the [Model Context Protocol](https://modelcontextprotocol.io/). + +Requires **`@modelcontextprotocol/sdk` v1.x** as a peer dependency. ## Installation ```bash -npm install @ext-modelcontextprotocol/interceptors +npm install mcp-ext-interceptors @modelcontextprotocol/sdk +``` + +## Overview + +``` +Client ──▶ Interceptor host ──▶ Application MCP server + ◀── (validate/mutate) ◀── (tools, resources, …) +``` + +An **interceptor host** is a normal MCP server that exposes `interceptors/list` and `interceptor/invoke`. It is not the same role as your tools/resources **backend** server. + +## Quick start — interceptor host (stdio) + +```typescript +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + registerInterceptorsOnServer, + InterceptionEvents, + validationSuccess, + type RegisteredInterceptor, +} from 'mcp-ext-interceptors'; + +const interceptors: RegisteredInterceptor[] = [ + { + descriptor: { + name: 'pii-validator', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => validationSuccess('request'), + }, +]; + +const server = new Server( + { name: 'my-interceptor-host', version: '1.0.0' }, + { capabilities: {} }, +); +registerInterceptorsOnServer(server, interceptors); + +await server.connect(new StdioServerTransport()); +``` + +Or use **`defineInterceptor`** for a C#-style handler definition: + +```typescript +import { defineInterceptor, InterceptionEvents } from 'mcp-ext-interceptors'; + +const entry = defineInterceptor( + { + name: 'email-redactor', + type: 'mutation', + events: [InterceptionEvents.ToolsCall], + phase: 'request', + priorityHint: -1000, + }, + (payload) => ({ + type: 'mutation', + phase: 'request', + modified: true, + payload: redactEmails(payload), + }), +); +``` + +## Quick start — client API + +```typescript +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { + listInterceptors, + invokeInterceptor, + executeInterceptorChainOnClient, +} from 'mcp-ext-interceptors'; + +const client = new Client({ name: 'app', version: '1.0.0' }, { capabilities: {} }); +await client.connect( + new StdioClientTransport({ + command: 'npx', + args: ['tsx', 'path/to/interceptor-server/src/index.ts'], + }), +); + +const listed = await listInterceptors(client); +const result = await invokeInterceptor(client, { + name: 'pii-validator', + event: 'tools/call', + phase: 'request', + payload: { name: 'my-tool', arguments: {} }, +}); + +const chain = await executeInterceptorChainOnClient(client, { + event: 'tools/call', + phase: 'request', + payload: { name: 'my-tool', arguments: { message: 'hello' } }, +}); ``` -## Usage +Chain execution is orchestrated in the SDK (`list` + ordered `invoke`); there is no `interceptor/executeChain` wire method. + +## Quick start — InterceptingMcpClient ```typescript -import { Interceptor } from '@ext-modelcontextprotocol/interceptors'; +import { InterceptingMcpClient } from 'mcp-ext-interceptors'; + +const gateway = new InterceptingMcpClient(backendClient, { + interceptorClient: interceptorHostClient, + events: ['tools/call'], +}); + +const result = await gateway.callTool('echo', { message: 'hello' }); +``` + +## Quick start — transparent proxy (`McpInterceptorGateway`) + +```typescript +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { McpInterceptorGateway } from 'mcp-ext-interceptors'; + +const gateway = new McpInterceptorGateway({ + backendClient, + interceptorClients: [interceptorHostClient], + events: ['tools/call'], +}); -// The MCP SDK is available as a peer dependency -import { Client, Server } from '@modelcontextprotocol/sdk'; +const server = new Server({ name: 'interceptor-proxy', version: '1.0.0' }, { capabilities: {} }); +gateway.configureServer(server); // before connect +gateway.registerNotificationForwarding(server); -// Example usage will be added as the implementation progresses +await server.connect(new StdioServerTransport()); ``` +Connecting clients use the proxy as the backend; the parent process spawns interceptor and backend servers over stdio (same pattern as the C# `TransparentProxySample`). + +## Capabilities + +Interceptor hosts advertise SEP **`capabilities.interceptor`** with `supportedEvents` (merged automatically by `registerInterceptorsOnServer`). + +The C# SDK in this repository uses `capabilities.extensions["interceptors"]` instead. Mixed deployments may need dual-read logic. + +## Examples + +From `typescript/sdk` after `npm run build` (examples import `dist/` from the local build): + +| Script | C# sample | Description | +|--------|-----------|-------------| +| `npm run example:interceptor-server` | `InterceptorServerSample` | Stdio interceptor host (PII validator, email redactor, logger sink) | +| `npm run example:interceptor-client` | `InterceptorClientSample` | Spawns the server via `StdioClientTransport`; list, invoke, chain | +| `npm run example:gateway` | `GatewaySample` | `InterceptingMcpClient` → interceptor host → everything server | +| `npm run example:transparent-proxy` | `TransparentProxySample` | Stdio transparent proxy (`McpInterceptorGateway`) | +| `npm run example:gateway-chain` | `GatewayChainSample` | Notes on multi-host ordering with `interceptorClients` | + +The client sample spawns the server process and talks over its stdio pipes (same pattern as the C# `StdioClientTransport` + `dotnet run --project …`). + ## Development ```bash -# Install dependencies npm install - -# Build npm run build - -# Run tests npm test - -# Lint npm run lint ``` ## License -Apache License 2.0 - See LICENSE file in the root directory for details. +Apache-2.0 — see the repository [LICENSE](https://github.com/modelcontextprotocol/ext-interceptors/blob/main/LICENSE). diff --git a/typescript/sdk/docs/design-and-implementation.md b/typescript/sdk/docs/design-and-implementation.md new file mode 100644 index 0000000..231a14e --- /dev/null +++ b/typescript/sdk/docs/design-and-implementation.md @@ -0,0 +1,541 @@ +# MCP Interceptors TypeScript SDK — Design and Implementation + +## Introduction + +This document is the **authoritative design reference** for the TypeScript Interceptor SDK shipped from `/typescript/sdk` as **`mcp-ext-interceptors`**. It defines what the package does, how it is structured, and how it integrates with the official MCP TypeScript SDK. + +Readers should use this document to implement or review the SDK. Normative interceptor protocol behavior is defined in [SEP-1763](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763) ([`docs/sep.md`](../../docs/sep.md) in this repository). Behavioral parity with the in-repo **C# Interceptor SDK** ([`csharp/sdk`](../../csharp/sdk)) is a primary goal. + +The plan distinguishes two relationships to the MCP TypeScript SDK: + +| Role | What | Where | +|------|------|--------| +| **Runtime dependency** | Code this package **imports at build and run time** | npm **`@modelcontextprotocol/sdk` v1.x** (`Client`, `Server` / `McpServer`, transports, protocol types) | +| **Structural reference** | How a mature MCP TypeScript SDK **organizes** client, server, protocol, tests, and public exports | Sibling repo **`typescript-sdk`** on **main** ([`../typescript-sdk`](../../typescript-sdk) when checked out beside this repo)—**not** a dependency | + +Implementation targets **v1** today while following **v2-shaped** module boundaries inside a **single** published package, so a later move to `@modelcontextprotocol/client` and `@modelcontextprotocol/server` is mostly adapter rewrites rather than a redesign. + +--- + +## 1. Scope and requirements + +### 1.1 Product scope + +- Implement the interceptor protocol from [SEP-1763](/docs/sep.md): wire methods, execution semantics, capability advertisement, and SDK conveniences (chain orchestration, hosting interceptors on an **interceptor host**, client helpers, and a **transparent gateway**). +- Provide **client**, **server**, and **gateway** APIs in one npm package, comparable in depth to the C# Interceptor SDK in this repository. +- Include **integration tests** where a client built with this SDK talks to a server built with this SDK over an in-process or equivalent transport, covering list, invoke, and chain execution. +- Ship **runnable examples** under `examples/`, modeled on the C# SDK’s [`csharp/sdk/samples`](../../csharp/sdk/samples) (see §10)—not part of the published npm artifact. + +### 1.2 Package and tooling + +- **Location:** `/typescript/sdk`, package name **`mcp-ext-interceptors`**. +- **Preserve** existing project configuration unless there is a strong, documented reason to change it: Node **≥20**, ESM, **`tsc`** → `dist/`, **Vitest**, **ESLint**, single root **`exports`** entry. +- **Publishing:** One npm package with logical modules under `src/` (`protocol`, `client`, `server`, `gateway`), not a multi-package monorepo like upstream `typescript-sdk`. +- **Dependencies:** **`peerDependencies`** on `@modelcontextprotocol/sdk` **^1.x**; matching **`devDependencies`** on the same range for reproducible CI and local tests. + +### 1.3 MCP TypeScript SDK strategy + +**Runtime (v1):** Import and use only **`@modelcontextprotocol/sdk`**. Do not depend on v2 workspace packages (`@modelcontextprotocol/client`, `@modelcontextprotocol/server`, `@modelcontextprotocol/core`). Use the MCP SDK for JSON-RPC sessions, transports, `Client` / `Server` / `McpServer`, and standard MCP types—do not reimplement core protocol plumbing. + +**Structure (v2 reference):** When making layout, naming, export, or handler-registration choices, align with conventions on **`typescript-sdk` main**: separate client vs server concerns, curated public `index` exports, Vitest layout, and explicit registration of non-spec JSON-RPC methods. Reconcile with that repo as it evolves; do not vendor or link it as a dependency. + +**Forward portability:** Keep interceptor-specific logic free of v1 types in `src/protocol` and `src/client/chain-orchestrator.ts`. Confine v1-only typing and handler registration to **`src/server/register-interceptors.ts`**, **`src/server/capabilities.ts`**, and **`src/client/client-extensions.ts`**. Avoid subclassing MCP SDK types in public interceptor APIs. + +**Later migration:** When v2 MCP packages are stable for consumers, change `peerDependencies` and those adapter modules to `@modelcontextprotocol/client` + `@modelcontextprotocol/server`. SEP DTOs, chain ordering, and gateway orchestration concepts should remain unchanged. + +### 1.4 Capability advertisement (TypeScript default) + +**Interceptor hosts** advertise support per the SEP: **`capabilities.interceptor`** with **`supportedEvents`**. The C# SDK in this repo uses **`ServerCapabilities.Extensions["interceptors"]`** instead; that difference is **documented for interoperability** (see §3) but **not** the TypeScript default wire shape. + +--- + +## 2. Interceptor model + +Per SEP-1763 (with terminology clarified for this SDK): + +### 2.1 Primitive vs hosts + +- An **interceptor** is an MCP **primitive** (governance logic for context operations)—analogous to tools, resources, and prompts, but with a different invocation model (see SEP). +- Interceptors are **discoverable** and **invocable** via JSON-RPC (`interceptors/list`, `interceptor/invoke`) on an **interceptor host**: an MCP-protocol **endpoint** that speaks the normal MCP session stack (`initialize`, JSON-RPC, transports) and advertises **`capabilities.interceptor`**. The SEP says interceptors are “hosted on MCP servers”; here **interceptor host** means that protocol role without implying the host is your **application MCP server**. +- An **application (backend) MCP server** is the server clients usually connect to for **tools**, **resources**, **prompts**, and related lifecycle events. It is a **different role** from an interceptor host. Deployments often use **client → interceptor host(s) → backend server** (see C# **`McpInterceptorGateway`** and SEP sidecar/proxy narrative). An interceptor host may expose **only** interceptor methods plus minimal MCP plumbing, or colocate interceptors with a backend—still two concerns: **governance primitives** vs **agent-facing capabilities**. +- **Validators** return pass/fail with severity and messages. **Mutators** return possibly modified payloads. **Sinks** are observe-only and non-blocking; the C# SDK treats **`sink`** as a first-class `InterceptorType`. +- Interceptors attach to **lifecycle events** (e.g. `tools/call`, `resources/read`, `prompts/get`, `llm/completion`) and a **phase** (`request`, `response`, or both). + +### 2.2 Chain execution + +- **Chain execution** calls **`interceptors/list`** on one or more interceptor hosts, then **`interceptor/invoke`** on the host that registered each interceptor, following the SEP trust-boundary-aware ordering: + - **Request (sending):** mutations (sequential by ascending `priorityHint`, name tie-break) → validations (parallel) → sinks (fire-and-forget). + - **Response (receiving):** validations (parallel) → sinks (fire-and-forget) → mutations (sequential). +- **`mode`:** `enforce` vs `audit` (shadow validation / mutation). **`failOpen`:** whether failures allow the message to proceed (per SEP rules). + +If the SEP text conflicts with itself, follow **normative** sections of [`docs/sep.md`](../../docs/sep.md) for wire methods and payloads. Where the SEP is silent or ambiguous, match behavior of the **C# reference** (e.g. **`interceptors/list`**, not `interceptor/list`). + +--- + +## 3. Wire protocol and capabilities + +### 3.1 JSON-RPC methods + +| Method | Params | Result | +|--------|--------|--------| +| `interceptors/list` | Optional `{ event?: string }` | `{ interceptors: Interceptor[] }` | +| `interceptor/invoke` | `name`, `event`, `phase`, `payload`, optional `config`, `context`, `timeoutMs` | Polymorphic result: `validation` \| `mutation` \| `sink` | + +### 3.2 Interceptor host capability (`initialize`) + +Per the SEP, interceptor hosts include: + +```json +{ + "capabilities": { + "interceptor": { + "supportedEvents": ["tools/call", "..."] + } + } +} +``` + +**C# interoperability:** The C# Interceptor SDK advertises the same logical data under **`capabilities.extensions["interceptors"]`** via the C# MCP SDK extension API. Mixed deployments may need dual-read logic or bridges; see package **README** (Capabilities section). + +**Discovery with v1 `@modelcontextprotocol/sdk` Client:** The server should set capability via `registerCapabilities` (see §5). The stock v1 **`Client`** parses `initialize` with `ServerCapabilitiesSchema`, which does **not** include `interceptor`, so **`getServerCapabilities().interceptor` is undefined** even when the server advertised it on the wire. Clients using this interceptor SDK should treat **`interceptors/list`** (or handling a standard JSON-RPC error when unsupported) as the reliable discovery path—not typed `interceptor` on parsed server capabilities. + +--- + +## 4. Reference material in this repository + +### 4.1 C# Interceptor SDK + +Primary behavioral reference for parity: + +| Area | C# concept | +|------|------------| +| Wire methods | `interceptors/list`, `interceptor/invoke` | +| Protocol DTOs | `Protocol/*` — descriptors, invoke/chain params, polymorphic `InterceptorResult`, events, phases, LLM payloads | +| Client | `McpClientInterceptorExtensions`, `InterceptorChainOrchestrator`, `InterceptingMcpClient` | +| Server | `InterceptorMessageFilter`, `McpServerInterceptorBuilderExtensions`, `ReflectionMcpServerInterceptor` | +| Gateway | `McpInterceptorGateway`, `InterceptorChainRunner`, transparent proxy + optional SEP passthrough | +| Init capability | `Extensions["interceptors"]` (not SEP’s top-level `interceptor` field) | + +TypeScript uses **`Server.setRequestHandler`** for extension methods where C# uses incoming **message filters**, because the v1 TypeScript SDK exposes handler registration publicly. + +### 4.2 TypeScript package today + +The SDK is **implemented** end-to-end: protocol types, client extensions and chain orchestration (including multi-host merge), interceptor host registration, reflection helpers, transparent gateway, runnable examples, and package **README**. **73 Vitest tests**; `npm run build`, `npm test`, and `npm run lint` are green. + +Build: `tsc -p tsconfig.build.json` → `dist/`; lint uses `tsconfig.eslint.json` (includes test files). + +**Optional / deferred:** golden JSON protocol fixtures vs C# `ProtocolTypesSerializationTests.cs`; full C# gateway test matrix parity (~33 cases in C#; TypeScript gateway integration has 13 cases in `mcp-interceptor-gateway.test.ts`). + +### 4.3 Known gaps vs C# (intentional or subset) + +| Area | C# | TypeScript today | +|------|-----|------------------| +| Init capability wire shape | `extensions["interceptors"]` | SEP `capabilities.interceptor` (documented; README) | +| Server registration | `InterceptorMessageFilter` on incoming messages | `Server.setRequestHandler` for extension methods (§4.1) | +| Builder / host helpers | `IMcpServerBuilder`, filter pipeline | `registerInterceptorsOnServer` only (no separate `interceptor-host.ts` helper) | +| `InterceptingMcpClient` tests | Broad gateway-overlap scenarios | One E2E: `tools/call` request mutation; API covers list/prompts/resources/subscribe | +| `McpInterceptorGateway` | ASP.NET `WithInterceptorGateway` builder extensions | `createAsync`, `interceptorServerConnections`, `interceptorServerConnectionResolver`, `dispose` (no DI builder) | +| Gateway tests | `McpInterceptorGatewayTests` + `GatewayComponentsTests` | 13 gateway integration tests (subset of full C# matrix) | +| Validation over transparent proxy | In-process exception types | JSON-RPC `McpError` to connecting clients (not `McpInterceptorValidationException`) | +| `serverInfo` override | `McpServerOptions.ServerInfo` | `McpInterceptorGatewayOptions.serverInfo` documented; v1 `Server` identity is fixed at `new Server(...)` construction | +| `GatewayChainSample` | Two stdio interceptor clients + nested `InterceptingMcpClient` | `examples/gateway-chain` is a **simplified** walkthrough; use `McpInterceptorGateway` with `interceptorClients: [first, second]` for ordered multi-host chains | +| LLM completion | Protocol + samples | `LlmCompletion*` **types** only; no `llm/completion` client/gateway wiring | +| Examples packaging | Per-sample `.csproj` | `interceptor-server` and `interceptor-client` have `package.json`; other examples are single `src/index.ts` + root `npm run example:*` | + +### 4.3.1 Differences from the C# SDK (intentional) + +#### Interceptor `mode`: `enforce` (TypeScript / SEP) vs `active` (C#) + +| | Wire / API value | Meaning | +|---|------------------|--------| +| **SEP-1763** | `enforce` \| `audit` | `enforce` = normal blocking and mutation application; `audit` = shadow / non-blocking | +| **C# Interceptor SDK** | `active` \| `audit` | `InterceptorMode.Active` serializes as `"active"` (same semantics as SEP `enforce`) | +| **TypeScript SDK** | `enforce` \| `audit` | Matches the SEP on the wire and in `InterceptorMode` | + +**Why TypeScript uses `enforce`:** The normative spec ([`docs/sep.md`](../../docs/sep.md)) names the default mode **`enforce`**. The in-repo C# SDK predates or diverges from that string and uses **`active`** instead. TypeScript defaults to **SEP-shaped** protocol types in this package (same rationale as `capabilities.interceptor` vs C# `extensions["interceptors"]`). + +**Interop:** When parsing descriptors from a C# interceptor host, Zod accepts **`mode: "active"`** and normalizes it to **`enforce`** before chain execution. New TypeScript hosts and samples should emit **`enforce`** or omit `mode` (orchestrator treats omitted as enforcing). Do not emit `active` from TypeScript servers. + +**C# team:** Aligning C# to `enforce` would match the SEP and this SDK; until then, mixed deployments should expect TS clients to accept `active` on read only. + +#### `priorityHint` per phase (SEP) vs scalar only (C#) + +| | `priorityHint` shape | Mutation ordering | +|---|----------------------|-------------------| +| **SEP-1763** | `number` **or** `{ request?: number; response?: number }` | `resolvePriority(interceptor, phase)`; missing side → `0`; validations ignore priority | +| **C# Interceptor SDK** | `int?` only | `.OrderBy(i => i.PriorityHint ?? 0)` — same value for both phases | +| **TypeScript SDK** | `PriorityHint` union + Zod parse | `resolvePriority()` in `chain-orchestrator` when sorting mutations | + +**Why TypeScript implements the object form:** The SEP allows different mutation order on request vs response (e.g. redact early on request, sanitize late on response). Scalar `priorityHint` still works unchanged. **`resolvePriority`** is exported from the package for hosts/tools that need the same rule. + +**C# team:** Add a `PriorityHint` DTO or `JsonElement` on `Interceptor`, implement the same `resolvePriority` in `InterceptorChainOrchestrator`, and optionally extend `McpServerInterceptorAttribute` if reflection should set per-phase values. + +### 4.4 MCP TypeScript SDK v2 (`typescript-sdk`) as structural reference + +Sibling repository **`../typescript-sdk`** (official MCP TypeScript SDK **v2** on **main**). Use it only for **conventions**, not as a runtime dependency. + +Relevant patterns to mirror: + +- **Modules:** `protocol`-like types in `src/protocol`; client patterns in `src/client`; server patterns in `src/server`; gateway in `src/gateway`. +- **Public API:** Named exports only from the package entry; no wildcard re-exports; new exports are API commitments (see `packages/client/src/index.ts` and `packages/server/src/index.ts`). +- **Custom methods (v2 shape):** `setRequestHandler('method', { params, result }, handler)` on `Protocol` / `Server`—the target shape when migrating off v1. +- **Tooling reference:** Vitest workspace, TypeScript 5.9.x, ESLint 9—upgrade this package’s Vitest only when there is clear benefit. + +Upstream publishes **multiple** packages (`@modelcontextprotocol/client`, `@modelcontextprotocol/server`, private `@modelcontextprotocol/core`). This interceptor package stays **one** artifact with v2-**shaped** folders inside `src/`. + +--- + +## 5. Integration with `@modelcontextprotocol/sdk` (v1) + +Pinned baseline for design decisions: **v1.29.x** (representative of **^1.x**). + +### 5.1 Surfaces used + +| Concern | v1 API | Interceptor usage | +|---------|--------|-------------------| +| Client | `Client`, transports (`InMemoryTransport`, stdio, HTTP/SSE as needed) | Extension requests; `InterceptingMcpClient` to backend + interceptor hosts | +| Interceptor host | `Server`, `McpServer` (`mcpServer.server` for registration) | `interceptors/list`, `interceptor/invoke`; capability merge on `initialize` | +| Types | `RequestSchema`, `ResultSchema`, MCP tool/resource/prompt types | Payloads and Zod schemas for extension methods | +| Out of scope | — | JSON-RPC framing, session lifecycle, core MCP method dispatch | + +Import subpaths: `@modelcontextprotocol/sdk/client`, `/server`, `/inMemory`, `/types` (as supported by the package exports map). + +### 5.2 Registering extension methods on the server + +v1 **`Server.setRequestHandler`** takes a **Zod request schema** (with a **`method` literal**), not v2’s three-argument `(method, { params, result }, handler)` form. + +```ts +import * as z from 'zod/v4'; +import { RequestSchema, ResultSchema } from '@modelcontextprotocol/sdk/types'; +import { Server } from '@modelcontextprotocol/sdk/server'; + +const InterceptorsListRequestSchema = RequestSchema.extend({ + method: z.literal('interceptors/list'), + params: z.object({ event: z.string().optional() }).optional(), +}); + +const InterceptorsListResultSchema = ResultSchema.extend({ + interceptors: z.array(/* Interceptor descriptor schema */), +}); + +server.setRequestHandler(InterceptorsListRequestSchema, async (request) => { + return { interceptors: [] }; +}); +``` + +Apply the same pattern for **`interceptor/invoke`** with params and result schemas aligned to SEP and `src/protocol` types. + +**Capability checks:** v1 `assertRequestHandlerCapability` only validates known spec methods. **`interceptors/list`** and **`interceptor/invoke`** are outside that switch and require **no** extra capability flag for handler registration to succeed. + +Implement registration in **`src/server/register-interceptors.ts`** only. + +### 5.3 Advertising `capabilities.interceptor` + +```ts +import type { ServerCapabilities } from '@modelcontextprotocol/sdk/types'; + +server.registerCapabilities({ + interceptor: { supportedEvents: ['tools/call'] }, +} as ServerCapabilities); +``` + +`mergeCapabilities` shallow-merges into internal server state; **`initialize`** returns `getCapabilities()` unchanged, so **`interceptor` appears on the wire**. The v1 TypeScript type for `ServerCapabilities` omits `interceptor`; confine the assertion to **`src/server/capabilities.ts`**. + +Do **not** use `extensions.interceptors` as the default—that matches C# but not the SEP shape chosen for TypeScript. + +### 5.4 Client extension requests + +```ts +await client.request( + { method: 'interceptors/list', params: {} }, + InterceptorsListResultSchema, +); +``` + +Implement in **`src/client/client-extensions.ts`**. Deserialize into types from **`src/protocol`**. + +### 5.5 Migration to v2 MCP packages (expected touch points) + +When switching runtime dependency to `@modelcontextprotocol/client` + `@modelcontextprotocol/server`: + +1. `package.json` peer (and dev) dependencies and import paths. +2. **`src/server/register-interceptors.ts`** — adopt `setRequestHandler(method, { params, result }, handler)`. +3. **`src/server/capabilities.ts`** — re-check capability types and merge APIs in v2 core. +4. **`src/client/client-extensions.ts`** — client `request` typing and imports. +5. Test transport imports. + +**Unchanged across migration:** `src/protocol/*`, `src/client/chain-orchestrator.ts`, gateway orchestration design, SEP ordering semantics. + +--- + +## 6. Package layout + +Single `package.json`, single published `"."` export (optional subpath exports only with deliberate `package.json` change): + +```text +src/ + index.ts # public barrel (named exports only) + protocol/ + constants.ts, types.ts, results.ts, zod-schemas.ts, errors.ts, llm-payload.ts + client/ + client-extensions.ts # list / invoke / executeChainOnClient + chain-orchestrator.ts # SEP chain ordering (no MCP SDK imports) + interceptor-chain-runner.ts # multi-host chain runner (client + gateway) + execute-interceptor-chain-on-clients.ts + merge-interceptor-chain-entries.ts + intercepting-client.ts # InterceptingMcpClient + server/ + register-interceptors.ts + capabilities.ts + interceptor-definition.ts + reflection.ts # defineInterceptor + gateway/ + mcp-interceptor-gateway.ts + gateway-proxy-configurator.ts + gateway-protocol-bridge.ts # optional exposeInterceptorProtocol + proxy-request.ts + __tests__/ + fixtures/hosts.ts # connectInterceptorHost, connectEchoBackend + integration/ # client-extensions, intercepting-client, mcp-interceptor-gateway + v1-server-wiring.test.ts +``` + +Cross-language naming: + +| C# (`ExecuteChainAsync`) | TypeScript | +|--------------------------|------------| +| Orchestrator only | `executeInterceptorChain(interceptors, invoker, params, signal?)` | +| `McpClient` + list + invoke | `executeInterceptorChainOnClient(client, params, signal?)` | + +Also: `listInterceptors`, `invokeInterceptor`. + +**Tests** (Vitest; co-located with sources where practical): + +- Unit: `src/protocol/protocol.test.ts`, `src/client/chain-orchestrator.test.ts`, `src/server/reflection.test.ts`, `src/server/register-interceptors.test.ts` +- Integration / E2E: `src/__tests__/integration/*.test.ts`, `src/__tests__/v1-server-wiring.test.ts` +- Shared fixtures: `src/__tests__/fixtures/hosts.ts` (`connectInterceptorHost`, `connectEchoBackend`) + +Optional future layout: `__tests__/protocol/` golden JSON; rename fixtures to `buildInterceptorHost` / `buildTestBackend` aliases. + +**Examples** (under `examples/`, not published in npm `"files"`; see §10): + +```text +examples/ + interceptor-server/ # package.json; ↔ InterceptorServerSample + interceptor-client/ # package.json; ↔ InterceptorClientSample + gateway/src/ # ↔ GatewaySample (InterceptingMcpClient) + transparent-proxy/src/ # ↔ TransparentProxySample (McpInterceptorGateway stdio) + gateway-chain/src/ # ↔ GatewayChainSample (simplified; see §4.3) +``` + +All runnable examples import `../../../dist/index.js` after `npm run build`. Root `package.json` scripts: `example:interceptor-server`, `example:interceptor-client`, `example:gateway`, `example:transparent-proxy`, `example:gateway-chain` (uses `tsx`). + +--- + +## 7. Public API + +### 7.1 Protocol + +- Types and constants aligned with SEP and C# `Protocol/`. +- **`InterceptorResult`:** discriminated union on `type: "validation" | "mutation" | "sink"` with safe parsing from JSON. + +### 7.2 Client + +- **`listInterceptors(client, params?)`**, **`invokeInterceptor(client, params)`** — wire calls on MCP `Client`. +- **`executeInterceptorChain(interceptors, invoker, params, signal?)`** — pure orchestrator; `invoker` typically calls `interceptor/invoke`. +- **`executeInterceptorChainOnClient(client, params, signal?)`** — discovers via `interceptors/list`, then orchestrates invokes (C# `ExecuteChainAsync`). +- **`executeInterceptorChainOnClients(clients, params, signal?)`** — multi-host list, merge, and chain (SEP merge semantics). +- **`InterceptingMcpClient`** wrapping backend + interceptor clients; same operation set as C# where applicable: `callTool`, `listTools`, `listPrompts`, `getPrompt`, `listResources`, `readResource`, `subscribeResource`, `listInterceptors`. + +### 7.3 Interceptor host (server-side) + +- Build an **interceptor host** using the MCP SDK’s `Server` / `McpServer`—a real MCP protocol endpoint, typically **not** the same process or role as the application backend that serves tools/resources. +- In-process interceptor registry (name → handler). +- **`registerInterceptorsOnServer(server, interceptors, options?)`**: installs wire handlers on that host, merges **`capabilities.interceptor`** from registered hooks’ events. + +### 7.4 Gateway + +- **`McpInterceptorGateway`** — transparent MCP proxy: MCP **server** toward clients, MCP **client(s)** toward the **application backend** and **interceptor host(s)**. +- **`configureServer(server)`** — mirror backend capabilities; register proxy handlers (tools, prompts, resources, completions/logging passthrough). Call **before** `server.connect()`. +- **`registerNotificationForwarding(proxyServer)`** — forward backend `list_changed` notifications when advertised. +- **`exposeInterceptorProtocol`** — optional aggregated `interceptors/list` / `interceptor/invoke` on the proxy via `GatewayInterceptorProtocolBridge`. +- Reuses **`InterceptorChainRunner`** (`executeInterceptorChainOnClients` with merged chain). + +Callers supply **already-connected** `Client` instances (stdio spawn is sample responsibility; see §10). + +--- + +## 8. Parity with C# SDK + +| Capability | C# | TypeScript | Notes | +|------------|-----|------------|-------| +| `interceptors/list` | Yes | Yes | | +| `interceptor/invoke` | Yes | Yes | | +| Polymorphic results + JSON round-trip | Yes | Yes | Golden JSON vs C# deferred | +| Chain semantics (order, audit, failOpen, timeout) | Yes | Yes | | +| Multi-host chain merge | Yes (see §11) | Yes | `executeInterceptorChainOnClients` | +| Client list / invoke / executeChain | Yes | Yes | | +| `capabilities.interceptor` on initialize (SEP) | No (`extensions["interceptors"]`) | Yes | TS default wire shape | +| `InterceptingMcpClient` operations | Yes | Yes | API parity; E2E tests mainly `tools/call` | +| Server registration ergonomics | Yes (`IMcpServerBuilder`) | Yes | `setRequestHandler`, not message filter | +| Reflection-style interceptors | Yes | Yes | `defineInterceptor` | +| LLM completion payload types | Yes (protocol) | Yes | Types only; no live `llm/completion` wiring | +| Transparent gateway + optional SEP exposure | Yes | Yes | Subset of C# gateway tests (§4.3) | +| Runnable examples (core set, §10) | Yes (`samples/`) | Yes | `gateway-chain` simplified vs C# | + +--- + +## 9. Testing + +Testing mirrors the C# Interceptor SDK test project: every shipped module has targeted tests, shared fixtures avoid copy-paste host setup, and **integration** tests use **`InMemoryTransport`** (or equivalent) for real JSON-RPC sessions—not only isolated pure functions. + +### 9.1 Layers + +| Layer | Transport / MCP session | Purpose | +|-------|-------------------------|---------| +| **Unit** | None | Pure logic: protocol parsing, chain ordering, capability merge helpers, registry mapping, result discrimination. Fast; no `Client`/`Server` lifecycle unless a one-line stub is unavoidable. | +| **Integration** | `InMemoryTransport` (paired client + server) | Wire handlers, `initialize` + capabilities on the wire, `listInterceptors` / `invokeInterceptor` against a real interceptor host built with this SDK. | +| **End-to-end (within Vitest)** | Multiple transports or gateway wiring | Full paths the product cares about: e.g. `InterceptingMcpClient` → interceptor host(s) → stub **backend**; gateway proxy forwarding and chain injection. Still in-process; not a separate test runner. | + +**End-to-end** = multi-role flows (client + host + backend). **Integration** = client ↔ single host. + +### 9.2 Shared fixtures (`src/__tests__/fixtures/hosts.ts`) + +- **`connectInterceptorHost(interceptors)`** — in-memory interceptor host via `registerInterceptorsOnServer`; returns `{ client, server, close }`. +- **`connectEchoBackend()`** — minimal backend with `tools/list` + `tools/call` echo; exposes `lastCall` for assertions. +- **Sample interceptors** — defined inline in tests (validator / mutator / sink) with predictable names and return shapes. +- **Golden JSON** — not implemented; deferred (optional alignment with C# `ProtocolTypesSerializationTests.cs`). + +Integration and gateway tests use these helpers so registration and capability setup stay consistent. + +### 9.3 Coverage by module + +| Module / API | Unit | Integration / E2E | C# reference | +|--------------|------|---------------------|--------------| +| `protocol/` (types, zod, `InterceptorResult` parsers) | Round-trip and omit-null JSON; enum/string wire shapes | — | `ProtocolTypesSerializationTests.cs` | +| `client/chain-orchestrator.ts` | Ordering, parallel validation, audit, failOpen, timeout, abort; fake invoker (no MCP) | Chain invoked via real `invokeInterceptor` callbacks in integration tests | `InterceptorChainOrchestratorTests.cs` | +| `client/client-extensions.ts` | — | `listInterceptors` / `invokeInterceptor` / `executeInterceptorChainOnClient` against fixture host | (orchestrator + client extensions) | +| `client/intercepting-client.ts` | — | E2E: `tools/call` request mutation reaches backend (expand to other operations optional) | `McpInterceptorGatewayTests.cs` (overlapping scenarios) | +| `server/register-interceptors.ts` | Registry → handler dispatch, error paths | `interceptors/list` filter by `event`; `interceptor/invoke` returns correct polymorphic result | — | +| `server/capabilities.ts` | `supportedEvents` derived from registered hooks | `initialize` / `getCapabilities()` includes `capabilities.interceptor` on wire | — | +| `server/reflection.ts` | Metadata extraction, invalid registration | Invoke reflected handler over transport | `ReflectionMcpServerInterceptorTests.cs` | +| `client/execute-interceptor-chain-on-clients.ts` | Merge, duplicate-name policy | Multi-host priority and routing | — | +| `gateway/` | `proxy-request.ts` chain wrapper | 13 tests in `mcp-interceptor-gateway.test.ts` | Subset of `GatewayComponentsTests.cs`, `McpInterceptorGatewayTests.cs` | + +### 9.4 Minimum scenarios (not exhaustive) + +**Protocol:** descriptor and invoke/chain param round-trips; validation / mutation / sink result unions; list result shape. + +**Client:** list returns registered interceptors; invoke returns each result type; executeChain applies order and aggregates failures; extensions send correct JSON-RPC method names. + +**Server:** host advertises `capabilities.interceptor`; list respects optional `event` filter; invoke dispatches to the right handler; unknown name / bad phase errors. + +**Integration (client ↔ host):** connect with `InMemoryTransport`; full list + invoke for at least one validator and one mutator. + +**End-to-end:** `InterceptingMcpClient` with fixture backend + host—covered for `tools/call`; other wrapped operations are API-complete but not all covered by dedicated E2E tests yet. + +**Gateway:** `tools/list` and `tools/call` forwarding, chain mutation, validation abort (as `McpError` over JSON-RPC), `exposeInterceptorProtocol` list aggregation, multi-host merge scenarios. + +**Current total:** 73 Vitest tests. CI runs all tests on every change. + +--- + +## 10. Examples (samples) + +Runnable examples mirror the **C# Interceptor SDK** layout in [`csharp/sdk/samples`](../../csharp/sdk/samples) and the walkthroughs in [`csharp/sdk/README.md`](../../csharp/sdk/README.md). They teach the same deployment patterns (interceptor host, direct client API, gateway, transparent proxy, chained gateways). **Scenario parity** with C# is the goal—not line-by-line ports. + +### 10.1 Principles + +- **Not published:** Examples live under `typescript/sdk/examples/` and are not included in the npm package `"files"` / `"exports"` for `mcp-ext-interceptors`. `interceptor-server` and `interceptor-client` include `"private": true` `package.json` files; `gateway`, `transparent-proxy`, and `gateway-chain` are single-entry scripts run from the SDK root. +- **Complement tests:** Vitest fixtures and integration tests (§9) remain the regression source of truth. Examples are copy-paste-friendly docs for humans; reuse the same interceptor names and behaviors as `src/__tests__/fixtures/` where practical. +- **Complement README:** Package `README.md` keeps short snippets; examples show **stdio spawn** wiring like the C# samples (parent process launches child; JSON-RPC over the child’s stdin/stdout). +- **C# reference column:** When implementing an example, read the matching C# `Program.cs` and treat it as the behavioral spec. + +### 10.2 Implemented examples + +| TypeScript example | C# sample | Script | What it demonstrates | +|--------------------|-----------|--------|----------------------| +| `examples/interceptor-server/` | `InterceptorServerSample` | `example:interceptor-server` | Stdio **interceptor host** (PII validator, email redactor, request-logger sink) | +| `examples/interceptor-client/` | `InterceptorClientSample` | `example:interceptor-client` | **Client** API; spawns interceptor-server via `StdioClientTransport` | +| `examples/gateway/` | `GatewaySample` | `example:gateway` | `InterceptingMcpClient` → interceptor host → `@modelcontextprotocol/server-everything` | +| `examples/transparent-proxy/` | `TransparentProxySample` | `example:transparent-proxy` | Stdio **`McpInterceptorGateway`**; parent spawns backend + interceptor host | +| `examples/gateway-chain/` | `GatewayChainSample` | `example:gateway-chain` | **Simplified:** documents multi-host ordering via `interceptorClients: [first, second]` on `McpInterceptorGateway` (C# runs two stdio interceptor processes + nested `InterceptingMcpClient`) | + +### 10.3 Out of scope + +| C# sample | Decision | +|-----------|----------| +| `AvatarMoodInterceptorSample` | **Not planned.** Pedagogy for `llm/completion` **sink** interceptors with live Anthropic calls and console UI—not MCP wiring. A TS port would add API keys, network, and non-CI dependencies without teaching the SDK’s core client/host/gateway paths. Sink behavior is covered in tests; optional tiny in-process sink demo only if needed later. | +| `ConfigDrivenGatewaySample` | **Optional.** C# treats `mcp-interceptors.json` as **sample-only** config, not a library format. Add a TS example only if we want the same “compose gateway from JSON” illustration; not required for parity with the five core samples above. | + +### 10.4 Stdio transport (match C#) + +| Role | Who spawns whom | Transport | +|------|-----------------|-----------| +| **Interceptor host** | Spawned as child | Stdio server (`WithStdioServerTransport` / equivalent). | +| **Client / gateway sample** | Parent process | `StdioClientTransport` with `command` + `args` (e.g. `node` / `tsx` + path to `interceptor-server`), same as C# `dotnet run --project …`. | +| **Transparent proxy** | Host app spawns proxy; proxy spawns peers | Stdio server toward host; stdio clients toward backend and interceptor host(s). | + +**ConfigDrivenGateway** (C#, optional for TS): outbound legs may use Streamable HTTP; gateway exposes stdio to the connecting host. + +### 10.5 Implementation notes + +- **Dependencies:** Examples depend on `mcp-ext-interceptors` (workspace/`file:`), `@modelcontextprotocol/sdk`, and Node stdio transports—no example-only dependency on the C# SDK. +- **Scripts:** Root `package.json` exposes `npm run example:*` for all five samples; spawning examples embed child `command`/`args` like C#. +- **Shared interceptors:** Prefer importing or duplicating minimal handler definitions from test fixtures so examples and tests do not diverge. +- **Gateway examples:** Spawn `interceptor-server` and stub backend via stdio client transport inside the gateway/proxy process—the same as C# `StdioClientTransport` + `dotnet run --project …`. + +--- + +## 11. Multi-host chain merge (C# port notes) + +This section documents multi-host chain behavior in the TypeScript SDK so the **C# Interceptor SDK** team can implement the same pattern if desired. It is not a breaking change to the wire protocol; it aligns client-side chain utilities with **SEP-1763 chain execution** ([`docs/sep.md`](../../docs/sep.md) § Chain Execution). + +### 11.1 Problem + +The SEP defines chain orchestration across **N MCP servers**: + +1. **Discover** — `interceptors/list` on one or more servers +2. **Merge & sort** — one combined chain (`priorityHint` ascending, name tie-break for mutations) +3. **Order by trust boundary** — request: mutations → validations → sinks; response: validations → sinks → mutations +4. **Execute** — `interceptor/invoke` on the **host that owns** each interceptor + +Both reference SDKs already implement step 3–4 for a **single flat interceptor list** (`InterceptorChainOrchestrator` / `executeInterceptorChain`). + +Previously, **multi-host** callers used `InterceptorChainRunner`, which ran a **full** `ExecuteChainAsync` **per host in series**. That preserves tiered pipelines but does **not** merge mutations globally by `priorityHint` across hosts (e.g. a mutator at `-1000` on host B must run before a mutator at `0` on host A per the SEP). + +### 11.2 TypeScript implementation + +| Piece | Role | +|-------|------| +| `executeInterceptorChain` | Unchanged — SEP execution model for one descriptor list + `invoker` | +| `executeInterceptorChainOnClient` | Unchanged surface — delegates to multi-host helper with one client | +| **`executeInterceptorChainOnClients`** | **New** — list each host → merge entries → `executeInterceptorChain` with routed `invoke` | +| `listInterceptorChainEntries` / `mergeInterceptorChainEntries` | **New** — discover + duplicate-name policy | +| `InterceptorChainRunner` | **Updated** — uses `executeInterceptorChainOnClients` instead of per-host full chains | + +**Duplicate interceptor names across hosts** + +- Default: **`duplicateNamePolicy: 'error'`** — throw `DuplicateInterceptorNameError` listing name and host labels (invoke routing is ambiguous because `interceptor/invoke` only carries `name`). +- Optional: **`'first-wins'`** — keep first entry in host array order (documented for tests / explicit tiering only). +- Deployment guidance: use **globally unique** interceptor names when using merged chains. + +### 11.3 SEP compliance + +- Normative chain steps 1–5 are implemented in the **multi-host entry point**; the orchestrator still enforces type/phase semantics (mutations sequential by `priorityHint`, validations parallel, sinks non-blocking). +- No new JSON-RPC methods; still `interceptors/list` + `interceptor/invoke` per owning server. +- `ChainEntry.server` in the SEP maps to `InterceptorChainEntry.client` (implementation-specific connection type). + +**Not changed:** chain `config` map forwarding, or wildcard event matching — same gaps as before (§4.3). Per-phase `priorityHint` is implemented in TypeScript (§4.3.1); C# still scalar-only. + +### 11.4 Suggested C# port + +1. Add `InterceptorChainEntry` (descriptor + `McpClient` + host label) and `ExecuteChainOnClientsAsync(IReadOnlyList hosts, ExecuteChainRequestParams request, …)`. +2. Implement merge + `DuplicateInterceptorNameException` (default throw on duplicate `Name`). +3. Call existing `InterceptorChainOrchestrator.ExecuteAsync` with an invoker that dispatches `InvokeInterceptorAsync` to the correct client. +4. Switch `InterceptorChainRunner` to use the new API (or document runner as “sequential per-host” if retaining old behavior behind a flag). +5. Add tests: global `PriorityHint` across two hosts; duplicate name error; optional `first-wins`. + +### 11.5 When to keep sequential per-host chains + +If product intent is **tiered hosts** (“always run security host chain, then logging host chain”) rather than **one global mutation order**, expose that as an explicit option (e.g. `ChainMergeMode.Merged` vs `PerHostSequential`) rather than overloading merge semantics. The SEP default for the chain **utility** is merge; tiered ordering is a deployment pattern clients can opt into. diff --git a/typescript/sdk/examples/gateway-chain/src/index.ts b/typescript/sdk/examples/gateway-chain/src/index.ts new file mode 100644 index 0000000..953d482 --- /dev/null +++ b/typescript/sdk/examples/gateway-chain/src/index.ts @@ -0,0 +1,60 @@ +/** + * Chained interceptors — two interceptor hosts in sequence before the backend. + * C# equivalent: GatewayChainSample (simplified with in-process ordering via gateway options). + */ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { + InterceptingMcpClient, + InterceptionEvents, +} from '../../../dist/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const interceptorServerEntry = join(here, '../../interceptor-server/src/index.ts'); + +async function main(): Promise { + console.log('=== Gateway chain sample ===\n'); + console.log( + 'This sample uses InterceptingMcpClient with one interceptor host.\n' + + 'For multiple hosts in order, use McpInterceptorGateway with interceptorClients: [first, second].\n', + ); + + const interceptorClient = new Client( + { name: 'chain-interceptor', version: '1.0.0' }, + { capabilities: {} }, + ); + await interceptorClient.connect( + new StdioClientTransport({ + command: 'npx', + args: ['tsx', interceptorServerEntry], + }), + ); + + const backendClient = new Client( + { name: 'chain-backend', version: '1.0.0' }, + { capabilities: {} }, + ); + await backendClient.connect( + new StdioClientTransport({ + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-everything'], + }), + ); + + const gateway = new InterceptingMcpClient(backendClient, { + interceptorClient, + events: [InterceptionEvents.ToolsCall], + }); + + const tools = await gateway.listTools(); + console.log(`Tools: ${tools.tools.map((t) => t.name).join(', ')}`); + + await gateway.close(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/sdk/examples/gateway/src/index.ts b/typescript/sdk/examples/gateway/src/index.ts new file mode 100644 index 0000000..eb4d0f9 --- /dev/null +++ b/typescript/sdk/examples/gateway/src/index.ts @@ -0,0 +1,70 @@ +/** + * Gateway sample — InterceptingMcpClient over a backend + interceptor host. + * C# equivalent: GatewaySample. + */ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { + InterceptingMcpClient, + InterceptionEvents, +} from '../../../dist/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const interceptorServerEntry = join(here, '../../interceptor-server/src/index.ts'); + +async function main(): Promise { + console.log('=== MCP Interceptors Gateway Sample (client API) ===\n'); + + const interceptorClient = new Client( + { name: 'gateway-interceptor', version: '1.0.0' }, + { capabilities: {} }, + ); + await interceptorClient.connect( + new StdioClientTransport({ + command: 'npx', + args: ['tsx', interceptorServerEntry], + }), + ); + + const backendClient = new Client( + { name: 'gateway-backend', version: '1.0.0' }, + { capabilities: {} }, + ); + await backendClient.connect( + new StdioClientTransport({ + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-everything'], + }), + ); + + const gateway = new InterceptingMcpClient(backendClient, { + interceptorClient, + events: [InterceptionEvents.ToolsCall], + }); + + const listed = await gateway.listInterceptors(); + console.log('Interceptors:'); + for (const i of listed.interceptors) { + console.log(` - ${i.name} (${i.type})`); + } + + console.log('\n── echo (should pass) ──'); + const ok = await gateway.callTool('echo', { message: 'Hello from gateway sample!' }); + console.log(' ', ok.content?.[0]); + + console.log('\n── echo with SSN (should be blocked) ──'); + try { + await gateway.callTool('echo', { message: 'My SSN is 123-45-6789' }); + } catch (err) { + console.log(' BLOCKED:', err instanceof Error ? err.message : err); + } + + await gateway.close(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/sdk/examples/interceptor-client/package.json b/typescript/sdk/examples/interceptor-client/package.json new file mode 100644 index 0000000..b9f438f --- /dev/null +++ b/typescript/sdk/examples/interceptor-client/package.json @@ -0,0 +1,9 @@ +{ + "name": "interceptor-client-example", + "private": true, + "type": "module", + "dependencies": { + "mcp-ext-interceptors": "file:../..", + "@modelcontextprotocol/sdk": "^1.29.0" + } +} diff --git a/typescript/sdk/examples/interceptor-client/src/index.ts b/typescript/sdk/examples/interceptor-client/src/index.ts new file mode 100644 index 0000000..ac95818 --- /dev/null +++ b/typescript/sdk/examples/interceptor-client/src/index.ts @@ -0,0 +1,113 @@ +#!/usr/bin/env node +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. +/** + * Interceptor Client Sample — spawns interceptor-server and exercises list / invoke / chain. + */ + +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { + executeInterceptorChainOnClient, + InterceptionEvents, + invokeInterceptor, + listInterceptors, + isMutationResult, + isValidationResult, +} from '../../../dist/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const serverEntry = join(here, '../../interceptor-server/src/index.ts'); + +console.log('=== MCP Interceptors Client Sample ===\n'); +console.log('[setup] Spawning interceptor server...'); + +const transport = new StdioClientTransport({ + command: 'npx', + args: ['tsx', serverEntry], + cwd: join(here, '../..'), +}); + +const client = new Client({ name: 'interceptor-client-sample', version: '1.0.0' }, { capabilities: {} }); +await client.connect(transport); +console.log('[setup] Connected.\n'); + +console.log('── Demo 1: List interceptors ──'); +const listResult = await listInterceptors(client); +for (const i of listResult.interceptors) { + const hooks = i.hooks + .map((h) => `${h.phase}:[${h.events.join(',')}]`) + .join('; '); + console.log(` ${i.name.padEnd(20)} type=${i.type.padEnd(12)} hooks=${hooks}`); + if (i.description) { + console.log(` ${''.padEnd(20)} ${i.description}`); + } +} + +console.log('\n── Demo 2: Invoke email-redactor ──'); +const emailPayload = { + name: 'echo', + arguments: { message: 'Contact alice@example.com' }, +}; +const redactResult = await invokeInterceptor(client, { + name: 'email-redactor', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: emailPayload, +}); +if (isMutationResult(redactResult)) { + console.log(` Modified: ${redactResult.modified}`); + console.log(` Payload: ${JSON.stringify(redactResult.payload)}`); +} + +console.log('\n── Demo 3: Invoke pii-validator (clean) ──'); +const safeResult = await invokeInterceptor(client, { + name: 'pii-validator', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { name: 'echo', arguments: { message: 'Hello world' } }, +}); +if (isValidationResult(safeResult)) { + console.log(` Valid: ${safeResult.valid}`); +} + +console.log('\n── Demo 4: Invoke pii-validator (PII) ──'); +const piiResult = await invokeInterceptor(client, { + name: 'pii-validator', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { name: 'echo', arguments: { message: 'My SSN is 123-45-6789' } }, +}); +if (isValidationResult(piiResult)) { + console.log(` Valid: ${piiResult.valid}`); + for (const msg of piiResult.messages ?? []) { + console.log(` [${msg.severity}] ${msg.message}`); + } +} + +console.log('\n── Demo 5: Execute chain ──'); +const chainResult = await executeInterceptorChainOnClient(client, { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { + name: 'echo', + arguments: { message: 'Email bob@corp.com about SSN' }, + }, + context: { traceId: crypto.randomUUID().replace(/-/g, '') }, +}); +console.log(` Status: ${chainResult.status}`); +console.log(` Duration: ${chainResult.totalDurationMs}ms`); +console.log(` Results: ${chainResult.results.length} interceptor(s)`); +if (chainResult.abortedAt) { + console.log(` Aborted: ${chainResult.abortedAt.interceptor} — ${chainResult.abortedAt.reason}`); +} +for (const r of chainResult.results) { + console.log(` ${(r.interceptor ?? '?').padEnd(20)} type=${r.type.padEnd(12)} duration=${r.durationMs ?? 0}ms`); +} +if (chainResult.finalPayload !== undefined) { + console.log(` Final payload: ${JSON.stringify(chainResult.finalPayload)}`); +} + +console.log('\n=== Done ==='); +await client.close(); diff --git a/typescript/sdk/examples/interceptor-server/package.json b/typescript/sdk/examples/interceptor-server/package.json new file mode 100644 index 0000000..74f73f6 --- /dev/null +++ b/typescript/sdk/examples/interceptor-server/package.json @@ -0,0 +1,9 @@ +{ + "name": "interceptor-server-example", + "private": true, + "type": "module", + "dependencies": { + "mcp-ext-interceptors": "file:../..", + "@modelcontextprotocol/sdk": "^1.29.0" + } +} diff --git a/typescript/sdk/examples/interceptor-server/src/index.ts b/typescript/sdk/examples/interceptor-server/src/index.ts new file mode 100644 index 0000000..d5fd59c --- /dev/null +++ b/typescript/sdk/examples/interceptor-server/src/index.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. +/** + * Interceptor Server Sample — stdio MCP host with validators, mutators, and a sink. + * Spawned by interceptor-client (or any MCP client using StdioClientTransport). + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { registerInterceptorsOnServer } from '../../../dist/index.js'; +import { sampleInterceptors } from './sample-interceptors.js'; + +const server = new Server( + { name: 'interceptor-server-sample', version: '1.0.0' }, + { capabilities: {} }, +); + +registerInterceptorsOnServer(server, sampleInterceptors); + +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/typescript/sdk/examples/interceptor-server/src/sample-interceptors.ts b/typescript/sdk/examples/interceptor-server/src/sample-interceptors.ts new file mode 100644 index 0000000..b7899fe --- /dev/null +++ b/typescript/sdk/examples/interceptor-server/src/sample-interceptors.ts @@ -0,0 +1,83 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { + InterceptionEvents, + validationFailure, + validationSuccess, + type RegisteredInterceptor, +} from '../../../dist/index.js'; + +function payloadText(payload: unknown): string { + return JSON.stringify(payload); +} + +export const sampleInterceptors: RegisteredInterceptor[] = [ + { + descriptor: { + name: 'pii-validator', + description: 'Checks tool call arguments for PII patterns', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: (params) => { + const json = payloadText(params.payload); + if (/ssn|social security/i.test(json)) { + return validationFailure(params.phase, { + path: '$.arguments', + message: 'Payload may contain Social Security Number data', + severity: 'error', + }); + } + return validationSuccess(params.phase); + }, + }, + { + descriptor: { + name: 'email-redactor', + description: 'Redacts email addresses from payloads', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + priorityHint: -1000, + }, + handler: (params) => { + const json = payloadText(params.payload); + const redacted = json.replace( + /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, + '[EMAIL_REDACTED]', + ); + if (redacted === json) { + return { type: 'mutation', phase: params.phase, modified: false, payload: params.payload }; + } + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: JSON.parse(redacted) as unknown, + }; + }, + }, + { + descriptor: { + name: 'request-logger', + description: 'Logs intercepted events to stderr', + type: 'sink', + hooks: [ + { events: [InterceptionEvents.All], phase: 'request' }, + { events: [InterceptionEvents.All], phase: 'response' }, + ], + }, + handler: (params) => { + const size = payloadText(params.payload).length; + const trace = params.context?.traceId ?? 'none'; + console.error( + `[interceptor] event=${params.event} phase=${params.phase} traceId=${trace} payloadBytes=${size}`, + ); + return { + type: 'sink', + phase: params.phase, + recorded: true, + metrics: { payloadBytes: size }, + }; + }, + }, +]; diff --git a/typescript/sdk/examples/transparent-proxy/src/index.ts b/typescript/sdk/examples/transparent-proxy/src/index.ts new file mode 100644 index 0000000..72f78d9 --- /dev/null +++ b/typescript/sdk/examples/transparent-proxy/src/index.ts @@ -0,0 +1,62 @@ +/** + * Transparent proxy — stdio MCP server that looks like the backend but runs interceptor chains. + * C# equivalent: TransparentProxySample. + */ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { + McpInterceptorGateway, + InterceptionEvents, +} from '../../../dist/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const interceptorServerEntry = join(here, '../../interceptor-server/src/index.ts'); + +async function main(): Promise { + const interceptorClient = new Client( + { name: 'InterceptorServer', version: '1.0.0' }, + { capabilities: {} }, + ); + await interceptorClient.connect( + new StdioClientTransport({ + command: 'npx', + args: ['tsx', interceptorServerEntry], + }), + ); + + const backendClient = new Client( + { name: 'EverythingServer', version: '1.0.0' }, + { capabilities: {} }, + ); + await backendClient.connect( + new StdioClientTransport({ + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-everything'], + }), + ); + + const gateway = new McpInterceptorGateway({ + backendClient, + interceptorClients: [interceptorClient], + events: [InterceptionEvents.ToolsCall], + }); + + const server = new Server( + { name: 'interceptor-proxy', version: '1.0.0' }, + { capabilities: {} }, + ); + gateway.configureServer(server); + gateway.registerNotificationForwarding(server); + + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/typescript/sdk/package-lock.json b/typescript/sdk/package-lock.json new file mode 100644 index 0000000..c564882 --- /dev/null +++ b/typescript/sdk/package-lock.json @@ -0,0 +1,4823 @@ +{ + "name": "mcp-ext-interceptors", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-ext-interceptors", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.0.0", + "tsx": "^4.19.0", + "typescript": "^5.0.0", + "vitest": "^2.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index ce494aa..a7ad3e3 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -1,12 +1,14 @@ { - "name": "@ext-modelcontextprotocol/interceptors", + "name": "mcp-ext-interceptors", "version": "0.1.0", "description": "TypeScript SDK for Model Context Protocol interceptors", "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.js", + "default": "./dist/index.js" } }, "main": "./dist/index.js", @@ -15,10 +17,15 @@ "node": ">=20" }, "scripts": { - "build": "tsc", + "build": "tsc -p tsconfig.build.json", "test": "vitest run", "test:watch": "vitest", "lint": "eslint src --ext .ts", + "example:interceptor-server": "npm run build && tsx examples/interceptor-server/src/index.ts", + "example:interceptor-client": "npm run build && tsx examples/interceptor-client/src/index.ts", + "example:gateway": "npm run build && tsx examples/gateway/src/index.ts", + "example:transparent-proxy": "npm run build && tsx examples/transparent-proxy/src/index.ts", + "example:gateway-chain": "npm run build && tsx examples/gateway-chain/src/index.ts", "prepublishOnly": "npm run build" }, "keywords": [ @@ -46,11 +53,13 @@ "@modelcontextprotocol/sdk": "^1.0.0" }, "devDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.0.0", "typescript": "^5.0.0", + "tsx": "^4.19.0", "vitest": "^2.0.0" } } \ No newline at end of file diff --git a/typescript/sdk/src/__tests__/fixtures/hosts.ts b/typescript/sdk/src/__tests__/fixtures/hosts.ts new file mode 100644 index 0000000..6b55d0b --- /dev/null +++ b/typescript/sdk/src/__tests__/fixtures/hosts.ts @@ -0,0 +1,168 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { + CallToolRequestSchema, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, + SubscribeRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { + registerInterceptorsOnServer, + type RegisteredInterceptor, +} from '../../server/register-interceptors.js'; + +export async function connectInterceptorHost( + interceptors: RegisteredInterceptor[], +): Promise<{ + client: Client; + server: Server; + close: () => Promise; +}> { + const server = new Server( + { name: 'test-interceptor-host', version: '0.0.0' }, + { capabilities: {} }, + ); + + registerInterceptorsOnServer(server, interceptors); + + const client = new Client({ name: 'test-client', version: '0.0.0' }, { capabilities: {} }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + return { + client, + server, + close: async () => { + await Promise.all([client.close(), server.close()]); + }, + }; +} + +export async function connectEchoBackend(): Promise<{ + client: Client; + server: Server; + close: () => Promise; + lastCall: { name: string; arguments?: Record }; +}> { + const lastCall = { name: '', arguments: undefined as Record | undefined }; + + const server = new Server( + { name: 'echo-backend', version: '0.0.0' }, + { capabilities: { tools: {} } }, + ); + + server.setRequestHandler(ListToolsRequestSchema, () => ({ + tools: [{ name: 'echo', description: 'echo', inputSchema: { type: 'object' } }], + })); + + server.setRequestHandler(CallToolRequestSchema, (request) => { + lastCall.name = request.params.name; + lastCall.arguments = request.params.arguments; + return { + content: [{ type: 'text', text: JSON.stringify(request.params.arguments ?? {}) }], + structuredContent: request.params.arguments ?? {}, + }; + }); + + const client = new Client({ name: 'gateway-client', version: '0.0.0' }, { capabilities: {} }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + return { + client, + server, + lastCall, + close: async () => { + await Promise.all([client.close(), server.close()]); + }, + }; +} + +/** Backend with tools, prompts, and subscribable resources for gateway / client E2E tests. */ +export async function connectRichBackend(): Promise<{ + client: Client; + server: Server; + close: () => Promise; + lastToolCall: { name: string; arguments?: Record }; + lastPromptGet: { name: string; arguments?: Record }; + subscription: { uri: string }; +}> { + const lastToolCall = { name: '', arguments: undefined as Record | undefined }; + const lastPromptGet = { name: '', arguments: undefined as Record | undefined }; + const subscription = { uri: '' }; + + const server = new Server( + { name: 'rich-backend', version: '0.0.0' }, + { + capabilities: { + tools: {}, + prompts: {}, + resources: { subscribe: true }, + }, + }, + ); + + server.setRequestHandler(ListToolsRequestSchema, () => ({ + tools: [{ name: 'echo', description: 'echo', inputSchema: { type: 'object' } }], + })); + + server.setRequestHandler(CallToolRequestSchema, (request) => { + lastToolCall.name = request.params.name; + lastToolCall.arguments = request.params.arguments; + const msg = request.params.arguments?.message; + return { + content: [{ type: 'text', text: `echo: ${String(msg ?? '')}` }], + }; + }); + + server.setRequestHandler(ListPromptsRequestSchema, () => ({ + prompts: [{ name: 'greet', description: 'greet' }], + })); + + server.setRequestHandler(GetPromptRequestSchema, (request) => { + lastPromptGet.name = request.params.name; + lastPromptGet.arguments = request.params.arguments; + return { + messages: [ + { + role: 'user' as const, + content: { type: 'text' as const, text: `Hello ${request.params.name}` }, + }, + ], + }; + }); + + server.setRequestHandler(ListResourcesRequestSchema, () => ({ + resources: [{ uri: 'resource://original', name: 'original' }], + })); + + server.setRequestHandler(ReadResourceRequestSchema, (request) => ({ + contents: [{ uri: request.params.uri, text: 'content' }], + })); + + server.setRequestHandler(SubscribeRequestSchema, (request) => { + subscription.uri = request.params.uri; + return {}; + }); + + const client = new Client({ name: 'rich-backend-client', version: '0.0.0' }, { capabilities: {} }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + return { + client, + server, + lastToolCall, + lastPromptGet, + subscription, + close: async () => { + await Promise.all([client.close(), server.close()]); + }, + }; +} diff --git a/typescript/sdk/src/__tests__/integration/client-extensions.test.ts b/typescript/sdk/src/__tests__/integration/client-extensions.test.ts new file mode 100644 index 0000000..0b03987 --- /dev/null +++ b/typescript/sdk/src/__tests__/integration/client-extensions.test.ts @@ -0,0 +1,104 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from '../../protocol/constants.js'; +import { validationSuccess } from '../../protocol/results.js'; +import { + executeInterceptorChainOnClient, + invokeInterceptor, + listInterceptors, +} from '../../client/client-extensions.js'; +import { connectInterceptorHost } from '../fixtures/hosts.js'; + +describe('client extensions integration', () => { + it('lists and invokes interceptors over InMemoryTransport', async () => { + const { client, close } = await connectInterceptorHost([ + { + descriptor: { + name: 'echo-validator', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => validationSuccess('request'), + }, + ]); + + const list = await listInterceptors(client); + expect(list.interceptors).toHaveLength(1); + expect(list.interceptors[0]?.name).toBe('echo-validator'); + + const result = await invokeInterceptor(client, { + name: 'echo-validator', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { x: 1 }, + }); + + expect(result.type).toBe('validation'); + if (result.type === 'validation') { + expect(result.valid).toBe(true); + } + + await close(); + }); + + it('executeInterceptorChainOnClient runs chain via list + invoke', async () => { + const { client, close } = await connectInterceptorHost([ + { + descriptor: { + name: 'mutator', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.All], phase: 'request' }], + priorityHint: 0, + }, + handler: (params) => ({ + type: 'mutation', + phase: params.phase, + modified: true, + payload: { mutated: true }, + }), + }, + ]); + + const chain = await executeInterceptorChainOnClient(client, { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }); + + expect(chain.status).toBe('success'); + expect(chain.finalPayload).toEqual({ mutated: true }); + expect(chain.results).toHaveLength(1); + + await close(); + }); + + it('executeInterceptorChainOnClient aborts when validation fails', async () => { + const { client, close } = await connectInterceptorHost([ + { + descriptor: { + name: 'blocker', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => + ({ + type: 'validation', + phase: 'request', + valid: false, + severity: 'error', + messages: [{ message: 'nope', severity: 'error' }], + }) as const, + }, + ]); + + const chain = await executeInterceptorChainOnClient(client, { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }); + + expect(chain.status).toBe('validation_failed'); + await close(); + }); +}); diff --git a/typescript/sdk/src/__tests__/integration/intercepting-client.test.ts b/typescript/sdk/src/__tests__/integration/intercepting-client.test.ts new file mode 100644 index 0000000..15805d1 --- /dev/null +++ b/typescript/sdk/src/__tests__/integration/intercepting-client.test.ts @@ -0,0 +1,145 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from '../../protocol/constants.js'; +import { McpInterceptorValidationException } from '../../protocol/errors.js'; +import { InterceptingMcpClient } from '../../client/intercepting-client.js'; +import { connectEchoBackend, connectInterceptorHost, connectRichBackend } from '../fixtures/hosts.js'; +import type { RegisteredInterceptor } from '../../server/register-interceptors.js'; + +describe('InterceptingMcpClient', () => { + it('applies request-phase mutation before tools/call reaches backend', async () => { + const mutator: RegisteredInterceptor = { + descriptor: { + name: 'arg-renamer', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + priorityHint: 0, + }, + handler: (params) => { + const p = params.payload as { name?: string; arguments?: Record }; + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { + name: p.name ?? 'echo', + arguments: { ...p.arguments, mutated: true }, + }, + }; + }, + }; + + const interceptor = await connectInterceptorHost([mutator]); + const backend = await connectEchoBackend(); + + const gateway = new InterceptingMcpClient(backend.client, { + interceptorClient: interceptor.client, + events: [InterceptionEvents.ToolsCall], + }); + + await gateway.callTool('echo', { message: 'hello' }); + + expect(backend.lastCall.name).toBe('echo'); + expect(backend.lastCall.arguments).toMatchObject({ message: 'hello', mutated: true }); + + await gateway.close(); + await interceptor.close(); + await backend.close(); + }); + + it('blocks tools/call with validation exception when interceptor rejects', async () => { + const blocker: RegisteredInterceptor = { + descriptor: { + name: 'blocker', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => + ({ + type: 'validation', + phase: 'request', + valid: false, + severity: 'error', + messages: [{ message: 'blocked', severity: 'error' }], + }) as const, + }; + + const interceptor = await connectInterceptorHost([blocker]); + const backend = await connectEchoBackend(); + const gateway = new InterceptingMcpClient(backend.client, { + interceptorClient: interceptor.client, + }); + + await expect(gateway.callTool('echo', {})).rejects.toBeInstanceOf( + McpInterceptorValidationException, + ); + expect(backend.lastCall.name).toBe(''); + + await gateway.close(); + await interceptor.close(); + await backend.close(); + }); + + it('proxies getPrompt through request-phase chain', async () => { + const mutator: RegisteredInterceptor = { + descriptor: { + name: 'prompt-tagger', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.PromptsGet], phase: 'request' }], + }, + handler: (params) => { + const p = params.payload as { name?: string; arguments?: Record }; + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { + name: p.name ?? 'greet', + arguments: { ...p.arguments, tagged: 'yes' }, + }, + }; + }, + }; + + const interceptor = await connectInterceptorHost([mutator]); + const backend = await connectRichBackend(); + const gateway = new InterceptingMcpClient(backend.client, { + interceptorClient: interceptor.client, + events: [InterceptionEvents.PromptsGet], + }); + + await gateway.getPrompt('greet', { who: 'world' }); + + expect(backend.lastPromptGet.name).toBe('greet'); + expect(backend.lastPromptGet.arguments).toMatchObject({ who: 'world', tagged: 'yes' }); + + await gateway.close(); + await interceptor.close(); + await backend.close(); + }); + + it('listInterceptors delegates to interceptor host client', async () => { + const interceptor = await connectInterceptorHost([ + { + descriptor: { + name: 'listed', + type: 'sink', + hooks: [{ events: [InterceptionEvents.All], phase: 'request' }], + }, + handler: () => ({ type: 'sink', phase: 'request', recorded: true }), + }, + ]); + const backend = await connectEchoBackend(); + const gateway = new InterceptingMcpClient(backend.client, { + interceptorClient: interceptor.client, + }); + + const listed = await gateway.listInterceptors(); + expect(listed.interceptors.some((i) => i.name === 'listed')).toBe(true); + + await gateway.close(); + await interceptor.close(); + await backend.close(); + }); +}); diff --git a/typescript/sdk/src/__tests__/integration/mcp-interceptor-gateway.test.ts b/typescript/sdk/src/__tests__/integration/mcp-interceptor-gateway.test.ts new file mode 100644 index 0000000..150bdc7 --- /dev/null +++ b/typescript/sdk/src/__tests__/integration/mcp-interceptor-gateway.test.ts @@ -0,0 +1,508 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import type { InterceptorClientTransport } from '../../gateway/mcp-interceptor-server-connection-options.js'; +import { McpError } from '@modelcontextprotocol/sdk/types.js'; +import { InterceptionEvents } from '../../protocol/constants.js'; +import { validationFailure, validationSuccess } from '../../protocol/results.js'; +import { listInterceptors } from '../../client/client-extensions.js'; +import { McpInterceptorGateway } from '../../gateway/mcp-interceptor-gateway.js'; +import { + connectEchoBackend, + connectInterceptorHost, + connectRichBackend, +} from '../fixtures/hosts.js'; +import { + registerInterceptorsOnServer, + type RegisteredInterceptor, +} from '../../server/register-interceptors.js'; + +async function startInterceptorHostTransport( + interceptors: RegisteredInterceptor[], +): Promise<{ transport: InterceptorClientTransport; close: () => Promise }> { + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const server = new Server( + { name: 'test-interceptor-host', version: '0.0.0' }, + { capabilities: {} }, + ); + registerInterceptorsOnServer(server, interceptors); + await server.connect(serverTransport); + return { + transport: clientTransport, + close: async () => { + await server.close(); + }, + }; +} + +async function connectGatewayProxy(gateway: McpInterceptorGateway): Promise<{ + proxyClient: Client; + close: () => Promise; +}> { + const proxyServer = new Server( + { name: 'test-proxy', version: '0.0.0' }, + { capabilities: {} }, + ); + + gateway.configureServer(proxyServer); + + const proxyClient = new Client({ name: 'proxy-client', version: '0.0.0' }, { capabilities: {} }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([proxyServer.connect(serverTransport), proxyClient.connect(clientTransport)]); + gateway.registerNotificationForwarding(proxyServer); + + return { + proxyClient, + close: async () => { + gateway.disposeNotificationForwarding(); + await Promise.all([proxyClient.close(), proxyServer.close()]); + }, + }; +} + +describe('McpInterceptorGateway', () => { + it('proxies tools/list from the backend', async () => { + const backend = await connectEchoBackend(); + const interceptor = await connectInterceptorHost([]); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [interceptor.client], + }); + + const proxy = await connectGatewayProxy(gateway); + const tools = await proxy.proxyClient.listTools(); + expect(tools.tools).toHaveLength(1); + expect(tools.tools[0]?.name).toBe('echo'); + + await proxy.close(); + await interceptor.close(); + await backend.close(); + }); + + it('runs tools/call through interceptor chains before the backend', async () => { + const mutator: RegisteredInterceptor = { + descriptor: { + name: 'arg-tagger', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: (params) => { + const p = params.payload as { name?: string; arguments?: Record }; + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { + name: p.name ?? 'echo', + arguments: { ...p.arguments, viaGateway: true }, + }, + }; + }, + }; + + const backend = await connectEchoBackend(); + const interceptor = await connectInterceptorHost([mutator]); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [interceptor.client], + events: [InterceptionEvents.ToolsCall], + }); + + const proxy = await connectGatewayProxy(gateway); + await proxy.proxyClient.callTool({ name: 'echo', arguments: { message: 'hi' } }); + + expect(backend.lastCall.arguments).toMatchObject({ message: 'hi', viaGateway: true }); + + await proxy.close(); + await interceptor.close(); + await backend.close(); + }); + + it('blocks tools/call when validation interceptors abort', async () => { + const blocker: RegisteredInterceptor = { + descriptor: { + name: 'blocker', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => + validationFailure('request', { + severity: 'error', + message: 'blocked by gateway test', + }), + }; + + const backend = await connectEchoBackend(); + const interceptor = await connectInterceptorHost([blocker]); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [interceptor.client], + }); + + const proxy = await connectGatewayProxy(gateway); + + await expect( + proxy.proxyClient.callTool({ name: 'echo', arguments: {} }), + ).rejects.toSatisfy((err: unknown) => { + expect(err).toBeInstanceOf(McpError); + expect((err as McpError).message).toMatch(/blocker.*reported invalid/i); + expect((err as McpError).message).toContain('blocked by gateway test'); + return true; + }); + + expect(backend.lastCall.name).toBe(''); + + await proxy.close(); + await interceptor.close(); + await backend.close(); + }); + + it('exposes aggregated interceptors/list when exposeInterceptorProtocol is true', async () => { + const listed: RegisteredInterceptor = { + descriptor: { + name: 'visible', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => validationSuccess('request'), + }; + + const backend = await connectEchoBackend(); + const interceptor = await connectInterceptorHost([listed]); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [interceptor.client], + exposeInterceptorProtocol: true, + }); + + const proxy = await connectGatewayProxy(gateway); + const result = await listInterceptors(proxy.proxyClient); + expect(result.interceptors.some((i) => i.name === 'visible')).toBe(true); + + await proxy.close(); + await interceptor.close(); + await backend.close(); + }); + + it('chains multiple interceptor hosts in order on tools/call', async () => { + const prependA: RegisteredInterceptor = { + descriptor: { + name: 'prepend-a', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + priorityHint: 0, + }, + handler: (params) => { + const p = params.payload as { name?: string; arguments?: Record }; + const msg = String(p.arguments?.message ?? ''); + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { + name: p.name ?? 'echo', + arguments: { ...p.arguments, message: `A:${msg}` }, + }, + }; + }, + }; + + const prependB: RegisteredInterceptor = { + descriptor: { + name: 'prepend-b', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + priorityHint: 0, + }, + handler: (params) => { + const p = params.payload as { name?: string; arguments?: Record }; + const msg = String(p.arguments?.message ?? ''); + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { + name: p.name ?? 'echo', + arguments: { ...p.arguments, message: `B:${msg}` }, + }, + }; + }, + }; + + const hostA = await connectInterceptorHost([prependA]); + const hostB = await connectInterceptorHost([prependB]); + const backend = await connectRichBackend(); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [hostA.client, hostB.client], + events: [InterceptionEvents.ToolsCall], + }); + + const proxy = await connectGatewayProxy(gateway); + const result = await proxy.proxyClient.callTool({ + name: 'echo', + arguments: { message: 'hello' }, + }); + + const blocks = result.content as Array<{ type: string; text?: string }> | undefined; + expect(blocks?.[0]?.text ?? '').toContain('B:A:hello'); + + await proxy.close(); + await hostA.close(); + await hostB.close(); + await backend.close(); + }); + + it('mirrors backend prompts capability and proxies getPrompt with interception', async () => { + const mutator: RegisteredInterceptor = { + descriptor: { + name: 'prompt-mutator', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.PromptsGet], phase: 'request' }], + }, + handler: (params) => { + const p = params.payload as { name?: string }; + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { name: `${p.name}-mutated` }, + }; + }, + }; + + const backend = await connectRichBackend(); + const interceptor = await connectInterceptorHost([mutator]); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [interceptor.client], + events: [InterceptionEvents.PromptsGet], + }); + + const proxy = await connectGatewayProxy(gateway); + await proxy.proxyClient.getPrompt({ name: 'greet' }); + + expect(backend.lastPromptGet.name).toBe('greet-mutated'); + + await proxy.close(); + await interceptor.close(); + await backend.close(); + }); + + it('rewrites resources/subscribe payload before backend receives it', async () => { + const rewriter: RegisteredInterceptor = { + descriptor: { + name: 'uri-rewriter', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ResourcesSubscribe], phase: 'request' }], + }, + handler: (params) => { + const p = params.payload as { uri?: string }; + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { + uri: (p.uri ?? '').replace('resource://original', 'resource://rewritten'), + }, + }; + }, + }; + + const backend = await connectRichBackend(); + const interceptor = await connectInterceptorHost([rewriter]); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [interceptor.client], + events: [InterceptionEvents.ResourcesSubscribe], + }); + + const proxy = await connectGatewayProxy(gateway); + await proxy.proxyClient.subscribeResource({ uri: 'resource://original' }); + + expect(backend.subscription.uri).toBe('resource://rewritten'); + + await proxy.close(); + await interceptor.close(); + await backend.close(); + }); + + it('does not expose interceptors/list on proxy by default', async () => { + const backend = await connectEchoBackend(); + const interceptor = await connectInterceptorHost([ + { + descriptor: { + name: 'hidden', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => validationSuccess('request'), + }, + ]); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [interceptor.client], + }); + + const proxy = await connectGatewayProxy(gateway); + + await expect(listInterceptors(proxy.proxyClient)).rejects.toThrow(); + + await proxy.close(); + await interceptor.close(); + await backend.close(); + }); + + it('createAsync connects interceptorServerConnections', async () => { + const listed: RegisteredInterceptor = { + descriptor: { + name: 'connected-via-transport', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => validationSuccess('request'), + }; + + const host = await startInterceptorHostTransport([listed]); + const backend = await connectEchoBackend(); + + const gateway = await McpInterceptorGateway.createAsync({ + backendClient: backend.client, + interceptorServerConnections: [{ transport: host.transport }], + exposeInterceptorProtocol: true, + }); + + const proxy = await connectGatewayProxy(gateway); + const result = await listInterceptors(proxy.proxyClient); + expect(result.interceptors.some((i) => i.name === 'connected-via-transport')).toBe(true); + + await proxy.close(); + await gateway.dispose(); + await host.close(); + await backend.close(); + }); + + it('rejects interceptorServerConnections on the constructor', () => { + expect( + () => + new McpInterceptorGateway({ + backendClient: {} as Client, + interceptorServerConnections: [{ transport: {} as InterceptorClientTransport }], + }), + ).toThrow(/createAsync/i); + }); + + it('runs tools/call via resolver-only transparent mode', async () => { + const mutator: RegisteredInterceptor = { + descriptor: { + name: 'resolver-mutator', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: (params) => { + const p = params.payload as { name?: string; arguments?: Record }; + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { + name: p.name ?? 'echo', + arguments: { ...p.arguments, viaResolver: true }, + }, + }; + }, + }; + + const host = await startInterceptorHostTransport([mutator]); + const backend = await connectEchoBackend(); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorServerConnectionResolver: (_context, event) => + Promise.resolve( + event === InterceptionEvents.ToolsCall + ? [{ transport: host.transport, connectionId: 'test-host' }] + : [], + ), + events: [InterceptionEvents.ToolsCall], + }); + + const proxy = await connectGatewayProxy(gateway); + await proxy.proxyClient.callTool({ name: 'echo', arguments: { message: 'hi' } }); + + expect(backend.lastCall.arguments).toMatchObject({ message: 'hi', viaResolver: true }); + + await proxy.close(); + await gateway.dispose(); + await host.close(); + await backend.close(); + }); + + it('rejects resolver with exposeInterceptorProtocol', () => { + expect( + () => + new McpInterceptorGateway({ + backendClient: {} as Client, + interceptorClients: [{} as Client], + interceptorServerConnectionResolver: () => Promise.resolve([]), + exposeInterceptorProtocol: true, + }), + ).toThrow(/exposeInterceptorProtocol/i); + }); + + it('aggregates interceptors from two hosts when exposeInterceptorProtocol is true', async () => { + const host1 = await connectInterceptorHost([ + { + descriptor: { + name: 'validator-1', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => validationSuccess('request'), + }, + ]); + const host2 = await connectInterceptorHost([ + { + descriptor: { + name: 'mutator-1', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: (params) => ({ + type: 'mutation', + phase: params.phase, + modified: false, + payload: params.payload, + }), + }, + ]); + const backend = await connectEchoBackend(); + + const gateway = new McpInterceptorGateway({ + backendClient: backend.client, + interceptorClients: [host1.client, host2.client], + exposeInterceptorProtocol: true, + }); + + const proxy = await connectGatewayProxy(gateway); + const result = await listInterceptors(proxy.proxyClient); + expect(result.interceptors).toHaveLength(2); + expect(result.interceptors.map((i) => i.name).sort()).toEqual(['mutator-1', 'validator-1']); + + await proxy.close(); + await host1.close(); + await host2.close(); + await backend.close(); + }); +}); diff --git a/typescript/sdk/src/__tests__/v1-server-wiring.test.ts b/typescript/sdk/src/__tests__/v1-server-wiring.test.ts new file mode 100644 index 0000000..f4b5433 --- /dev/null +++ b/typescript/sdk/src/__tests__/v1-server-wiring.test.ts @@ -0,0 +1,86 @@ +/** + * Integration smoke test: v1 MCP SDK server registration for interceptors/list and SEP capabilities. + */ +import { describe, it, expect } from 'vitest'; +import * as z from 'zod/v4'; +import { Client } from '@modelcontextprotocol/sdk/client'; +import { Server } from '@modelcontextprotocol/sdk/server'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'; +import { + RequestSchema, + ResultSchema, + type ServerCapabilities, +} from '@modelcontextprotocol/sdk/types'; + +const InterceptorsListRequestSchema = RequestSchema.extend({ + method: z.literal('interceptors/list'), + params: z + .object({ + event: z.string().optional(), + }) + .optional(), +}); + +const InterceptorsListResultSchema = ResultSchema.extend({ + interceptors: z.array( + z.object({ + name: z.string(), + type: z.literal('validation'), + }), + ), +}); + +describe('MCP SDK v1 server wiring', () => { + it('handles interceptors/list and advertises capabilities.interceptor on the server', async () => { + const server = new Server( + { name: 'spike-interceptor-server', version: '0.0.0' }, + { capabilities: {} }, + ); + + server.registerCapabilities({ + interceptor: { + supportedEvents: ['tools/call'], + }, + } as ServerCapabilities); + + server.setRequestHandler(InterceptorsListRequestSchema, () => ({ + interceptors: [{ name: 'test-validator', type: 'validation' as const }], + })); + + const client = new Client( + { name: 'spike-client', version: '0.0.0' }, + { capabilities: {} }, + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport), + ]); + + // Server retains SEP capability after merge (source of truth for what we advertise). + type CapsWithInterceptor = ServerCapabilities & { + interceptor?: { supportedEvents: string[] }; + }; + // v1 Server.getCapabilities() is untyped in @modelcontextprotocol/sdk + // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- extension field not in ServerCapabilitiesSchema + const serverCaps = server.getCapabilities() as CapsWithInterceptor; + expect(serverCaps).toMatchObject({ + interceptor: { supportedEvents: ['tools/call'] }, + }); + + // v1 Client parses initialize with ServerCapabilitiesSchema (no `interceptor` field). + const caps = client.getServerCapabilities(); + expect(caps?.interceptor).toBeUndefined(); + + const listResult = await client.request( + { method: 'interceptors/list', params: {} }, + InterceptorsListResultSchema, + ); + + expect(listResult.interceptors).toHaveLength(1); + expect(listResult.interceptors[0]?.name).toBe('test-validator'); + + await Promise.all([client.close(), server.close()]); + }); +}); diff --git a/typescript/sdk/src/client/chain-orchestrator.test.ts b/typescript/sdk/src/client/chain-orchestrator.test.ts new file mode 100644 index 0000000..0dcdecb --- /dev/null +++ b/typescript/sdk/src/client/chain-orchestrator.test.ts @@ -0,0 +1,592 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from '../protocol/constants.js'; +import { validationFailure, validationSuccess } from '../protocol/results.js'; +import type { + ExecuteChainRequestParams, + Interceptor, + InterceptorPhase, + InterceptorResult, + InterceptorType, + InvokeInterceptorRequestParams, +} from '../protocol/types.js'; +import { executeInterceptorChain, type InterceptorInvoker } from './chain-orchestrator.js'; + +type Handler = InterceptorInvoker; + +function runChain( + entries: Array<{ descriptor: Interceptor; handler: Handler }>, + chainParams: ExecuteChainRequestParams, + signal?: AbortSignal, +) { + const byName = new Map(entries.map((e) => [e.descriptor.name, e.handler])); + return executeInterceptorChain( + entries.map((e) => e.descriptor), + (req, s) => byName.get(req.name)!(req, s), + chainParams, + signal, + ); +} + +function createEntry( + name: string, + type: InterceptorType, + handler: ( + req: InvokeInterceptorRequestParams, + signal?: AbortSignal, + ) => InterceptorResult | Promise, + options: { + priorityHint?: Interceptor['priorityHint']; + events?: string[]; + phase?: InterceptorPhase | 'both'; + mode?: Interceptor['mode']; + failOpen?: boolean; + } = {}, +) { + const events = options.events ?? [InterceptionEvents.All]; + const phase = options.phase ?? 'both'; + const hooks = + phase === 'both' + ? [ + { events: [...events], phase: 'request' as const }, + { events: [...events], phase: 'response' as const }, + ] + : [{ events: [...events], phase }]; + + const descriptor: Interceptor = { + name, + type, + hooks, + mode: options.mode, + failOpen: options.failOpen, + priorityHint: options.priorityHint, + }; + + const wrapped: Handler = async (req, signal) => { + const result = await handler(req, signal); + result.phase = req.phase; + return result; + }; + + return { descriptor, handler: wrapped }; +} + +describe('executeInterceptorChain', () => { + it('request phase runs mutations before validations before sinks', async () => { + const order: string[] = []; + + const result = await runChain( + [ + createEntry('mut-1', 'mutation', () => { + order.push('mutation'); + return { type: 'mutation', phase: 'request', modified: true, payload: { mutated: true } }; + }), + createEntry('val-1', 'validation', () => { + order.push('validation'); + return validationSuccess('request'); + }), + createEntry('sink-1', 'sink', () => { + order.push('sink'); + return { type: 'sink', phase: 'request', recorded: true }; + }), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { original: true }, + }, + ); + + expect(result.status).toBe('success'); + expect(order).toEqual(['mutation', 'validation', 'sink']); + expect(result.finalPayload).toEqual({ mutated: true }); + }); + + it('response phase runs validations before sinks before mutations', async () => { + const order: string[] = []; + + await runChain( + [ + createEntry('mut-1', 'mutation', () => { + order.push('mutation'); + return { type: 'mutation', phase: 'response', modified: false }; + }), + createEntry('val-1', 'validation', () => { + order.push('validation'); + return validationSuccess('response'); + }), + createEntry('sink-1', 'sink', () => { + order.push('sink'); + return { type: 'sink', phase: 'response', recorded: true }; + }), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'response', + payload: { test: true }, + }, + ); + + expect(order).toEqual(['validation', 'sink', 'mutation']); + }); + + it('orders mutations by priorityHint then name', async () => { + const order: string[] = []; + + await runChain( + [ + createEntry( + 'mut-high', + 'mutation', + () => { + order.push('high'); + return { type: 'mutation', phase: 'request', modified: false }; + }, + { priorityHint: 100 }, + ), + createEntry( + 'mut-low', + 'mutation', + () => { + order.push('low'); + return { type: 'mutation', phase: 'request', modified: false }; + }, + { priorityHint: -100 }, + ), + createEntry( + 'mut-default', + 'mutation', + () => { + order.push('default'); + return { type: 'mutation', phase: 'request', modified: false }; + }, + { priorityHint: 0 }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(order).toEqual(['low', 'default', 'high']); + }); + + it('orders mutations by phase-specific priorityHint', async () => { + const requestOrder: string[] = []; + const responseOrder: string[] = []; + + await runChain( + [ + createEntry( + 'mut-a', + 'mutation', + () => { + requestOrder.push('a'); + return { type: 'mutation', phase: 'request', modified: false }; + }, + { priorityHint: { request: 100, response: 100 } }, + ), + createEntry( + 'mut-b', + 'mutation', + () => { + requestOrder.push('b'); + return { type: 'mutation', phase: 'request', modified: false }; + }, + { priorityHint: { request: -100, response: -100 } }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(requestOrder).toEqual(['b', 'a']); + + await runChain( + [ + createEntry( + 'mut-a', + 'mutation', + () => { + responseOrder.push('a'); + return { type: 'mutation', phase: 'response', modified: false }; + }, + { priorityHint: { request: 100, response: 100 } }, + ), + createEntry( + 'mut-b', + 'mutation', + () => { + responseOrder.push('b'); + return { type: 'mutation', phase: 'response', modified: false }; + }, + { priorityHint: { request: -100, response: -100 } }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'response', + payload: {}, + }, + ); + + expect(responseOrder).toEqual(['b', 'a']); + }); + + it('aborts on validation error in enforce mode', async () => { + const result = await runChain( + [ + createEntry('strict', 'validation', () => + validationFailure('request', { + message: 'Required field missing', + severity: 'error', + }), + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('validation_failed'); + expect(result.abortedAt?.interceptor).toBe('strict'); + expect(result.abortedAt?.type).toBe('validation'); + }); + + it('audit mutation records but does not apply payload', async () => { + const result = await runChain( + [ + createEntry( + 'shadow', + 'mutation', + () => ({ + type: 'mutation', + phase: 'request', + modified: true, + payload: { shadowed: true }, + }), + { mode: 'audit' }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { original: true }, + }, + ); + + expect(result.status).toBe('success'); + expect(result.finalPayload).toEqual({ original: true }); + expect(result.results[0]).toMatchObject({ type: 'mutation', modified: true }); + }); + + it('fail-open mutation continues after crash', async () => { + const result = await runChain( + [ + createEntry( + 'crashing', + 'mutation', + () => { + throw new Error('boom'); + }, + { failOpen: true }, + ), + createEntry( + 'following', + 'mutation', + (req) => { + const payload = { ...(req.payload as object), reached: true }; + return { type: 'mutation', phase: 'request', modified: true, payload }; + }, + { priorityHint: 1 }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('success'); + expect(result.finalPayload).toMatchObject({ reached: true }); + }); + + it('swallows sink failures', async () => { + const result = await runChain( + [ + createEntry('failing-sink', 'sink', () => { + throw new Error('sink failure'); + }), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('success'); + expect(result.results).toHaveLength(1); + expect(result.results[0]).toMatchObject({ type: 'sink', recorded: false }); + }); + + it('filters by event and phase', async () => { + const result = await runChain( + [ + createEntry( + 'tools-only', + 'validation', + () => validationSuccess('request'), + { events: [InterceptionEvents.ToolsCall], phase: 'request' }, + ), + createEntry( + 'prompts-only', + 'validation', + () => validationSuccess('request'), + { events: [InterceptionEvents.PromptsGet], phase: 'request' }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.results).toHaveLength(1); + expect(result.results[0]?.interceptor).toBe('tools-only'); + }); + + it('filters interceptors by phase only', async () => { + const result = await runChain( + [ + createEntry( + 'request-only', + 'validation', + () => validationSuccess('request'), + { phase: 'request' }, + ), + createEntry( + 'response-only', + 'validation', + () => validationSuccess('response'), + { phase: 'response' }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.results).toHaveLength(1); + expect(result.results[0]?.interceptor).toBe('request-only'); + }); + + it('chains mutation payloads sequentially', async () => { + const result = await runChain( + [ + createEntry( + 'mut-1', + 'mutation', + (req) => { + const p = { ...(req.payload as object), step1: true }; + return { type: 'mutation', phase: 'request', modified: true, payload: p }; + }, + { priorityHint: 0 }, + ), + createEntry( + 'mut-2', + 'mutation', + (req) => { + expect((req.payload as { step1?: boolean }).step1).toBe(true); + const p = { ...(req.payload as object), step2: true }; + return { type: 'mutation', phase: 'request', modified: true, payload: p }; + }, + { priorityHint: 1 }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { original: true }, + }, + ); + + expect(result.status).toBe('success'); + expect(result.finalPayload).toMatchObject({ original: true, step1: true, step2: true }); + }); + + it('audit validation does not block on error', async () => { + const result = await runChain( + [ + createEntry( + 'auditor', + 'validation', + () => + validationFailure('request', { + message: 'Audit-only violation', + severity: 'error', + }), + { mode: 'audit' }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('success'); + expect(result.validationSummary?.errors).toBe(1); + expect(result.abortedAt).toBeUndefined(); + }); + + it('mutation interceptor returning validation failure blocks chain', async () => { + const result = await runChain( + [ + createEntry( + 'policy-mut', + 'mutation', + () => + validationFailure('request', { + message: 'Blocked by policy', + severity: 'error', + }), + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('validation_failed'); + expect(result.abortedAt?.interceptor).toBe('policy-mut'); + expect(result.abortedAt?.reason).toBe('Blocked by policy'); + }); + + it('fail-closed mutation halts chain on crash', async () => { + const result = await runChain( + [ + createEntry('crashing', 'mutation', () => { + throw new Error('boom'); + }), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('mutation_failed'); + expect(result.abortedAt?.interceptor).toBe('crashing'); + expect(result.abortedAt?.type).toBe('mutation'); + }); + + it('fail-open validation continues after crash', async () => { + const result = await runChain( + [ + createEntry( + 'crashing-validator', + 'validation', + () => { + throw new Error('validator boom'); + }, + { failOpen: true }, + ), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('success'); + }); + + it('fail-closed validation halts chain on crash', async () => { + const result = await runChain( + [ + createEntry('crashing-validator', 'validation', () => { + throw new Error('validator boom'); + }), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('validation_failed'); + expect(result.abortedAt?.interceptor).toBe('crashing-validator'); + }); + + it('validation summary counts severities', async () => { + const result = await runChain( + [ + createEntry('val', 'validation', () => ({ + type: 'validation', + phase: 'request', + valid: true, + messages: [ + { message: 'Info', severity: 'info' }, + { message: 'Warn 1', severity: 'warn' }, + { message: 'Warn 2', severity: 'warn' }, + ], + })), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + + expect(result.status).toBe('success'); + expect(result.validationSummary).toEqual({ errors: 0, warnings: 2, infos: 1 }); + }); + + it('aborts with timeout status when chain timeout elapses', async () => { + const result = await runChain( + [ + createEntry('slow-mut', 'mutation', (_req, signal) => { + return new Promise((_resolve, reject) => { + if (!signal) { + reject(new Error('expected abort signal')); + return; + } + signal.addEventListener( + 'abort', + () => reject(signal.reason ?? new Error('timeout')), + { once: true }, + ); + }); + }), + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + timeoutMs: 50, + }, + ); + + expect(result.status).toBe('timeout'); + }); +}); diff --git a/typescript/sdk/src/client/chain-orchestrator.ts b/typescript/sdk/src/client/chain-orchestrator.ts new file mode 100644 index 0000000..8f8fa50 --- /dev/null +++ b/typescript/sdk/src/client/chain-orchestrator.ts @@ -0,0 +1,372 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. +// Use of this source code is governed by a Apache-2.0 +// license that can be found in the LICENSE file. + +import { InterceptionEvents } from '../protocol/constants.js'; +import { resolvePriority } from '../protocol/resolve-priority.js'; +import { + isMutationResult, + isValidationResult, +} from '../protocol/results.js'; +import type { + ChainAbortInfo, + ChainValidationSummary, + ExecuteChainRequestParams, + Interceptor, + InterceptorChainResult, + InterceptorChainStatus, + InterceptorResult, + InvokeInterceptorRequestParams, + SinkInterceptorResult, +} from '../protocol/types.js'; + +export type InterceptorInvoker = ( + params: InvokeInterceptorRequestParams, + signal?: AbortSignal, +) => Promise; + +export function matchesEvent(hookEvents: string[], requestEvent: string): boolean { + for (const ev of hookEvents) { + if (ev === InterceptionEvents.All || ev === requestEvent) { + return true; + } + } + return false; +} + +function filterInterceptors( + interceptors: Interceptor[], + chainParams: ExecuteChainRequestParams, +): Interceptor[] { + const nameFilter = chainParams.interceptors; + const out: Interceptor[] = []; + + for (const descriptor of interceptors) { + if (nameFilter && nameFilter.length > 0 && !nameFilter.includes(descriptor.name)) { + continue; + } + + let matchesHook = false; + for (const hook of descriptor.hooks) { + if (hook.phase !== chainParams.phase) { + continue; + } + if (matchesEvent(hook.events, chainParams.event)) { + matchesHook = true; + break; + } + } + if (matchesHook) { + out.push(descriptor); + } + } + + return out; +} + +function createInvokeParams( + descriptor: Interceptor, + chainParams: ExecuteChainRequestParams, + currentPayload: unknown, +): InvokeInterceptorRequestParams { + return { + name: descriptor.name, + event: chainParams.event, + phase: chainParams.phase, + payload: currentPayload, + context: chainParams.context, + timeoutMs: chainParams.timeoutMs, + }; +} + +function clonePayload(payload: unknown): unknown { + if (payload === undefined) { + return payload; + } + return structuredClone(payload); +} + +function isAbortError(err: unknown, signal?: AbortSignal): boolean { + if (signal?.aborted) { + return true; + } + return err instanceof Error && err.name === 'TimeoutError'; +} + +function chainSignal(outer?: AbortSignal, timeoutMs?: number): AbortSignal | undefined { + if (timeoutMs == null && outer == null) { + return undefined; + } + if (timeoutMs == null) { + return outer; + } + const timeoutSignal = AbortSignal.timeout(timeoutMs); + if (outer == null) { + return timeoutSignal; + } + return AbortSignal.any([outer, timeoutSignal]); +} + +export async function executeInterceptorChain( + interceptors: Interceptor[], + invoker: InterceptorInvoker, + chainParams: ExecuteChainRequestParams, + signal?: AbortSignal, +): Promise { + const started = Date.now(); + const results: InterceptorResult[] = []; + const summary: ChainValidationSummary = { errors: 0, warnings: 0, infos: 0 }; + let currentPayload = chainParams.payload; + let abortInfo: ChainAbortInfo | undefined; + let status: InterceptorChainStatus = 'success'; + + const applicable = filterInterceptors(interceptors, chainParams); + const mutations = applicable + .filter((i) => i.type === 'mutation') + .sort( + (a, b) => + resolvePriority(a, chainParams.phase) - resolvePriority(b, chainParams.phase) || + a.name.localeCompare(b.name), + ); + const validations = applicable.filter((i) => i.type === 'validation'); + const sinks = applicable.filter((i) => i.type === 'sink'); + + const ct = chainSignal(signal, chainParams.timeoutMs); + + try { + if (chainParams.phase === 'request') { + const mut = await executeMutations(mutations, invoker, chainParams, currentPayload, results, ct); + currentPayload = mut.payload; + status = mut.status; + abortInfo = mut.abortInfo; + if (status !== 'success') { + return finish(); + } + + const val = await executeValidations(validations, invoker, chainParams, currentPayload, results, summary, ct); + status = val.status; + abortInfo = val.abortInfo; + if (status !== 'success') { + return finish(); + } + + await executeSinks(sinks, invoker, chainParams, currentPayload, results, ct); + } else { + const val = await executeValidations(validations, invoker, chainParams, currentPayload, results, summary, ct); + status = val.status; + abortInfo = val.abortInfo; + if (status !== 'success') { + return finish(); + } + + await executeSinks(sinks, invoker, chainParams, currentPayload, results, ct); + + const mut = await executeMutations(mutations, invoker, chainParams, currentPayload, results, ct); + currentPayload = mut.payload; + status = mut.status; + abortInfo = mut.abortInfo; + } + } catch (err) { + if (isAbortError(err, ct)) { + status = 'timeout'; + } else { + throw err; + } + } + + return finish(); + + function finish(): InterceptorChainResult { + return { + status, + event: chainParams.event, + phase: chainParams.phase, + results, + finalPayload: currentPayload, + validationSummary: summary, + totalDurationMs: Date.now() - started, + abortedAt: abortInfo, + }; + } +} + +async function executeMutations( + mutations: Interceptor[], + invoker: InterceptorInvoker, + chainParams: ExecuteChainRequestParams, + initialPayload: unknown, + results: InterceptorResult[], + signal?: AbortSignal, +): Promise<{ payload: unknown; status: InterceptorChainStatus; abortInfo?: ChainAbortInfo }> { + let currentPayload = initialPayload; + + for (const descriptor of mutations) { + const isAudit = descriptor.mode === 'audit'; + const failOpen = descriptor.failOpen === true; + + try { + const invokeParams = createInvokeParams(descriptor, chainParams, currentPayload); + const sw = Date.now(); + const result = await invoker(invokeParams, signal); + result.interceptor = descriptor.name; + result.durationMs = Date.now() - sw; + results.push(result); + + if (!isAudit && isValidationResult(result)) { + if (!result.valid && result.severity === 'error') { + return { + payload: currentPayload, + status: 'validation_failed', + abortInfo: { + interceptor: descriptor.name, + reason: result.messages?.[0]?.message ?? 'Validation failed', + type: 'validation', + }, + }; + } + continue; + } + + if (!isAudit && isMutationResult(result) && result.modified && result.payload !== undefined) { + currentPayload = clonePayload(result.payload); + } + } catch (err) { + if (isAbortError(err, signal)) { + throw err; + } + if (isAudit || failOpen) { + continue; + } + return { + payload: currentPayload, + status: 'mutation_failed', + abortInfo: { + interceptor: descriptor.name, + reason: err instanceof Error ? err.message : String(err), + type: 'mutation', + }, + }; + } + } + + return { payload: currentPayload, status: 'success' }; +} + +async function executeValidations( + validations: Interceptor[], + invoker: InterceptorInvoker, + chainParams: ExecuteChainRequestParams, + currentPayload: unknown, + results: InterceptorResult[], + summary: ChainValidationSummary, + signal?: AbortSignal, +): Promise<{ status: InterceptorChainStatus; abortInfo?: ChainAbortInfo }> { + const completed = await Promise.all( + validations.map(async (descriptor) => { + try { + const invokeParams = createInvokeParams(descriptor, chainParams, currentPayload); + const sw = Date.now(); + const result = await invoker(invokeParams, signal); + result.interceptor = descriptor.name; + result.durationMs = Date.now() - sw; + return { descriptor, result, error: null as Error | null }; + } catch (err) { + if (isAbortError(err, signal)) { + throw err; + } + return { + descriptor, + result: null as InterceptorResult | null, + error: err instanceof Error ? err : new Error(String(err)), + }; + } + }), + ); + + for (const { descriptor, result, error } of completed) { + const isAudit = descriptor.mode === 'audit'; + const failOpen = descriptor.failOpen === true; + + if (error) { + if (isAudit || failOpen) { + continue; + } + return { + status: 'validation_failed', + abortInfo: { + interceptor: descriptor.name, + reason: error.message, + type: 'validation', + }, + }; + } + + if (!result) { + continue; + } + results.push(result); + + if (isValidationResult(result)) { + if (result.messages) { + for (const msg of result.messages) { + switch (msg.severity) { + case 'error': + summary.errors++; + break; + case 'warn': + summary.warnings++; + break; + case 'info': + summary.infos++; + break; + } + } + } + + if (!isAudit && !result.valid && result.severity === 'error') { + return { + status: 'validation_failed', + abortInfo: { + interceptor: descriptor.name, + reason: result.messages?.[0]?.message ?? 'Validation failed', + type: 'validation', + }, + }; + } + } + } + + return { status: 'success' }; +} + +async function executeSinks( + sinks: Interceptor[], + invoker: InterceptorInvoker, + chainParams: ExecuteChainRequestParams, + currentPayload: unknown, + results: InterceptorResult[], + signal?: AbortSignal, +): Promise { + const completed = await Promise.all( + sinks.map(async (descriptor) => { + try { + const invokeParams = createInvokeParams(descriptor, chainParams, currentPayload); + const sw = Date.now(); + const result = await invoker(invokeParams, signal); + result.interceptor = descriptor.name; + result.durationMs = Date.now() - sw; + return result; + } catch { + const fallback: SinkInterceptorResult = { + type: 'sink', + phase: chainParams.phase, + interceptor: descriptor.name, + recorded: false, + }; + return fallback; + } + }), + ); + + results.push(...completed); +} diff --git a/typescript/sdk/src/client/client-extensions.ts b/typescript/sdk/src/client/client-extensions.ts new file mode 100644 index 0000000..85b40a0 --- /dev/null +++ b/typescript/sdk/src/client/client-extensions.ts @@ -0,0 +1,53 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. +// Use of this source code is governed by a Apache-2.0 +// license that can be found in the LICENSE file. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { executeInterceptorChainOnClients } from './execute-interceptor-chain-on-clients.js'; +import { InterceptorRequestMethods } from '../protocol/constants.js'; +import { + InterceptorResultSchema, + ListInterceptorsResultSchema, +} from '../protocol/zod-schemas.js'; +import type { + ExecuteChainRequestParams, + InterceptorChainResult, + InterceptorResult, + InvokeInterceptorRequestParams, + ListInterceptorsRequestParams, + ListInterceptorsResult, +} from '../protocol/types.js'; + +export async function listInterceptors( + client: Client, + params?: ListInterceptorsRequestParams, +): Promise { + return client.request( + { + method: InterceptorRequestMethods.InterceptorsList, + params: (params ?? {}) as unknown as Record, + }, + ListInterceptorsResultSchema, + ); +} + +export async function invokeInterceptor( + client: Client, + params: InvokeInterceptorRequestParams, +): Promise { + return client.request( + { + method: InterceptorRequestMethods.InterceptorInvoke, + params: params as unknown as Record, + }, + InterceptorResultSchema, + ); +} + +export async function executeInterceptorChainOnClient( + client: Client, + params: ExecuteChainRequestParams, + signal?: AbortSignal, +): Promise { + return executeInterceptorChainOnClients([{ client }], params, { signal }); +} diff --git a/typescript/sdk/src/client/execute-interceptor-chain-on-clients.test.ts b/typescript/sdk/src/client/execute-interceptor-chain-on-clients.test.ts new file mode 100644 index 0000000..077b12b --- /dev/null +++ b/typescript/sdk/src/client/execute-interceptor-chain-on-clients.test.ts @@ -0,0 +1,103 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from '../protocol/constants.js'; +import { DuplicateInterceptorNameError } from '../protocol/errors.js'; +import { connectInterceptorHost } from '../__tests__/fixtures/hosts.js'; +import type { RegisteredInterceptor } from '../server/register-interceptors.js'; +import { executeInterceptorChainOnClients } from './execute-interceptor-chain-on-clients.js'; +import { + listInterceptorChainEntries, + mergeInterceptorChainEntries, +} from './merge-interceptor-chain-entries.js'; + +function mutator( + name: string, + priorityHint: number, + prefix: string, +): RegisteredInterceptor { + return { + descriptor: { + name, + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + priorityHint, + }, + handler: (params) => { + const p = params.payload as { arguments?: { message?: string } }; + const msg = String(p.arguments?.message ?? ''); + return { + type: 'mutation', + phase: params.phase, + modified: true, + payload: { ...p, arguments: { ...p.arguments, message: `${prefix}${msg}` } }, + }; + }, + }; +} + +describe('executeInterceptorChainOnClients', () => { + it('orders mutations by global priorityHint across hosts', async () => { + const low = await connectInterceptorHost([mutator('low-priority', 100, 'L:')]); + const high = await connectInterceptorHost([mutator('high-priority', -100, 'H:')]); + + const result = await executeInterceptorChainOnClients( + [ + { client: low.client, label: 'low-host' }, + { client: high.client, label: 'high-host' }, + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { name: 'echo', arguments: { message: 'x' } }, + }, + ); + + expect(result.status).toBe('success'); + const payload = result.finalPayload as { arguments?: { message?: string } }; + expect(payload.arguments?.message).toBe('L:H:x'); + + await low.close(); + await high.close(); + }); + + it('throws DuplicateInterceptorNameError by default', async () => { + const hostA = await connectInterceptorHost([mutator('dup', 0, 'A:')]); + const hostB = await connectInterceptorHost([mutator('dup', 0, 'B:')]); + + const entries = await listInterceptorChainEntries([ + { client: hostA.client, label: 'a' }, + { client: hostB.client, label: 'b' }, + ]); + + expect(() => mergeInterceptorChainEntries(entries)).toThrow(DuplicateInterceptorNameError); + + await hostA.close(); + await hostB.close(); + }); + + it('supports first-wins duplicate policy', async () => { + const hostA = await connectInterceptorHost([mutator('dup', 0, 'A:')]); + const hostB = await connectInterceptorHost([mutator('dup', 0, 'B:')]); + + const result = await executeInterceptorChainOnClients( + [ + { client: hostA.client, label: 'a' }, + { client: hostB.client, label: 'b' }, + ], + { + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { name: 'echo', arguments: { message: 'x' } }, + }, + { duplicateNamePolicy: 'first-wins' }, + ); + + expect(result.status).toBe('success'); + const payload = result.finalPayload as { arguments?: { message?: string } }; + expect(payload.arguments?.message).toBe('A:x'); + + await hostA.close(); + await hostB.close(); + }); +}); diff --git a/typescript/sdk/src/client/execute-interceptor-chain-on-clients.ts b/typescript/sdk/src/client/execute-interceptor-chain-on-clients.ts new file mode 100644 index 0000000..a7b2012 --- /dev/null +++ b/typescript/sdk/src/client/execute-interceptor-chain-on-clients.ts @@ -0,0 +1,49 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { executeInterceptorChain } from './chain-orchestrator.js'; +import { invokeInterceptor } from './client-extensions.js'; +import type { InterceptorChainHost, MergeInterceptorChainEntriesOptions } from './interceptor-chain-entry.js'; +import { + clientByInterceptorName, + interceptorsFromEntries, + listInterceptorChainEntries, + mergeInterceptorChainEntries, +} from './merge-interceptor-chain-entries.js'; +import type { ExecuteChainRequestParams, InterceptorChainResult } from '../protocol/types.js'; + +export interface ExecuteInterceptorChainOnClientsOptions extends MergeInterceptorChainEntriesOptions { + signal?: AbortSignal; +} + +/** + * SEP-aligned multi-host chain: discover on each client, merge, then run + * {@link executeInterceptorChain} with routed `interceptor/invoke` calls. + */ +export async function executeInterceptorChainOnClients( + hosts: InterceptorChainHost[], + params: ExecuteChainRequestParams, + options?: ExecuteInterceptorChainOnClientsOptions, +): Promise { + if (hosts.length === 0) { + throw new Error('At least one interceptor host is required'); + } + + const listed = await listInterceptorChainEntries(hosts, { event: params.event }); + const entries = mergeInterceptorChainEntries(listed, { + duplicateNamePolicy: options?.duplicateNamePolicy, + }); + const clients = clientByInterceptorName(entries); + + return executeInterceptorChain( + interceptorsFromEntries(entries), + (invokeParams) => { + const client = clients.get(invokeParams.name); + if (!client) { + throw new Error(`No host registered for interceptor '${invokeParams.name}'`); + } + return invokeInterceptor(client, invokeParams); + }, + params, + options?.signal, + ); +} diff --git a/typescript/sdk/src/client/intercepting-client.ts b/typescript/sdk/src/client/intercepting-client.ts new file mode 100644 index 0000000..b4c581a --- /dev/null +++ b/typescript/sdk/src/client/intercepting-client.ts @@ -0,0 +1,280 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { + CompatibilityCallToolResultSchema, + type CallToolResult, + type GetPromptResult, + type ListPromptsResult, + type ListResourcesResult, + type ListToolsResult, + type ReadResourceResult, +} from '@modelcontextprotocol/sdk/types.js'; +import { InterceptionEvents } from '../protocol/constants.js'; +import type { + InvokeInterceptorContext, + ListInterceptorsRequestParams, + ListInterceptorsResult, +} from '../protocol/types.js'; +import { listInterceptors } from './client-extensions.js'; +import { InterceptorChainRunner } from './interceptor-chain-runner.js'; + +export interface InterceptingMcpClientOptions { + interceptorClient: Client; + /** When omitted or empty, all configured events are intercepted. */ + events?: string[]; + timeoutMs?: number; + defaultContext?: InvokeInterceptorContext; +} + +/** + * Gateway-style client: runs interceptor chains on request/response, then forwards to the backend. + */ +export class InterceptingMcpClient { + readonly inner: Client; + readonly interceptorClient: Client; + private readonly chainRunner: InterceptorChainRunner; + + constructor(inner: Client, options: InterceptingMcpClientOptions) { + this.inner = inner; + this.interceptorClient = options.interceptorClient; + this.chainRunner = new InterceptorChainRunner({ + interceptorClients: [options.interceptorClient], + events: options.events, + timeoutMs: options.timeoutMs, + defaultContext: options.defaultContext, + }); + } + + async callTool( + name: string, + args?: Record, + signal?: AbortSignal, + ): Promise { + const resultSchema = CompatibilityCallToolResultSchema; + if (!this.chainRunner.shouldIntercept(InterceptionEvents.ToolsCall)) { + return (await this.inner.callTool( + { name, arguments: args }, + resultSchema, + { signal }, + )) as CallToolResult; + } + + const callParams = { name, arguments: args }; + let requestPayload: unknown = callParams; + + requestPayload = await this.chainRunner.runChainPhaseOrThrow( + 'tools/call', + InterceptionEvents.ToolsCall, + 'request', + requestPayload, + signal, + ); + + const mutated = + typeof requestPayload === 'object' && requestPayload !== null && 'name' in requestPayload + ? (requestPayload as { name: string; arguments?: Record }) + : callParams; + + const result = await this.inner.callTool( + { name: mutated.name, arguments: mutated.arguments ?? args }, + resultSchema, + { signal }, + ); + + const processedResponse = await this.chainRunner.runChainPhaseOrThrow( + 'tools/call', + InterceptionEvents.ToolsCall, + 'response', + result, + signal, + ); + + return processedResponse as CallToolResult; + } + + async listTools(signal?: AbortSignal): Promise { + if (!this.chainRunner.shouldIntercept(InterceptionEvents.ToolsList)) { + return this.inner.listTools(undefined, { signal }); + } + + await this.chainRunner.runChainPhaseOrThrow( + 'tools/list', + InterceptionEvents.ToolsList, + 'request', + {}, + signal, + ); + + const result = await this.inner.listTools(undefined, { signal }); + + await this.chainRunner.runChainPhaseOrThrow( + 'tools/list', + InterceptionEvents.ToolsList, + 'response', + result, + signal, + ); + + return result; + } + + async listPrompts(signal?: AbortSignal): Promise { + if (!this.chainRunner.shouldIntercept(InterceptionEvents.PromptsList)) { + return this.inner.listPrompts(undefined, { signal }); + } + + await this.chainRunner.runChainPhaseOrThrow( + 'prompts/list', + InterceptionEvents.PromptsList, + 'request', + {}, + signal, + ); + + const result = await this.inner.listPrompts(undefined, { signal }); + + await this.chainRunner.runChainPhaseOrThrow( + 'prompts/list', + InterceptionEvents.PromptsList, + 'response', + result, + signal, + ); + + return result; + } + + async getPrompt( + name: string, + args?: Record, + signal?: AbortSignal, + ): Promise { + if (!this.chainRunner.shouldIntercept(InterceptionEvents.PromptsGet)) { + return this.inner.getPrompt({ name, arguments: args }, { signal }); + } + + const getParams = { name, arguments: args }; + let requestPayload: unknown = getParams; + + requestPayload = await this.chainRunner.runChainPhaseOrThrow( + 'prompts/get', + InterceptionEvents.PromptsGet, + 'request', + requestPayload, + signal, + ); + + const mutated = + typeof requestPayload === 'object' && requestPayload !== null && 'name' in requestPayload + ? (requestPayload as { name: string; arguments?: Record }) + : getParams; + + const result = await this.inner.getPrompt( + { name: mutated.name, arguments: mutated.arguments ?? args }, + { signal }, + ); + + const processed = await this.chainRunner.runChainPhaseOrThrow( + 'prompts/get', + InterceptionEvents.PromptsGet, + 'response', + result, + signal, + ); + + return processed as GetPromptResult; + } + + async listResources(signal?: AbortSignal): Promise { + if (!this.chainRunner.shouldIntercept(InterceptionEvents.ResourcesList)) { + return this.inner.listResources(undefined, { signal }); + } + + await this.chainRunner.runChainPhaseOrThrow( + 'resources/list', + InterceptionEvents.ResourcesList, + 'request', + {}, + signal, + ); + + const result = await this.inner.listResources(undefined, { signal }); + + await this.chainRunner.runChainPhaseOrThrow( + 'resources/list', + InterceptionEvents.ResourcesList, + 'response', + result, + signal, + ); + + return result; + } + + async readResource(uri: string, signal?: AbortSignal): Promise { + if (!this.chainRunner.shouldIntercept(InterceptionEvents.ResourcesRead)) { + return this.inner.readResource({ uri }, { signal }); + } + + const readParams = { uri }; + let requestPayload: unknown = readParams; + + requestPayload = await this.chainRunner.runChainPhaseOrThrow( + 'resources/read', + InterceptionEvents.ResourcesRead, + 'request', + requestPayload, + signal, + ); + + const mutated = + typeof requestPayload === 'object' && requestPayload !== null && 'uri' in requestPayload + ? (requestPayload as { uri: string }) + : readParams; + + const result = await this.inner.readResource({ uri: mutated.uri }, { signal }); + + const processed = await this.chainRunner.runChainPhaseOrThrow( + 'resources/read', + InterceptionEvents.ResourcesRead, + 'response', + result, + signal, + ); + + return processed as ReadResourceResult; + } + + async subscribeResource(uri: string, signal?: AbortSignal): Promise { + if (!this.chainRunner.shouldIntercept(InterceptionEvents.ResourcesSubscribe)) { + await this.inner.subscribeResource({ uri }, { signal }); + return; + } + + let requestPayload: unknown = { uri }; + + requestPayload = await this.chainRunner.runChainPhaseOrThrow( + 'resources/subscribe', + InterceptionEvents.ResourcesSubscribe, + 'request', + requestPayload, + signal, + ); + + const mutated = + typeof requestPayload === 'object' && requestPayload !== null && 'uri' in requestPayload + ? (requestPayload as { uri: string }) + : { uri }; + + await this.inner.subscribeResource({ uri: mutated.uri }, { signal }); + } + + listInterceptors(params?: ListInterceptorsRequestParams): Promise { + return listInterceptors(this.interceptorClient, params); + } + + async close(): Promise { + await Promise.all([this.inner.close(), this.interceptorClient.close()]); + } +} diff --git a/typescript/sdk/src/client/interceptor-chain-entry.ts b/typescript/sdk/src/client/interceptor-chain-entry.ts new file mode 100644 index 0000000..f1c72e0 --- /dev/null +++ b/typescript/sdk/src/client/interceptor-chain-entry.ts @@ -0,0 +1,30 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { Interceptor } from '../protocol/types.js'; + +/** One interceptor descriptor and the MCP client for the host that registered it. */ +export interface InterceptorChainEntry { + descriptor: Interceptor; + client: Client; + /** Diagnostic label for this host (duplicate-name errors, logging). */ + hostLabel: string; +} + +/** Identifies an interceptor host when listing or merging chain entries. */ +export interface InterceptorChainHost { + client: Client; + /** Shown in errors; defaults to `host-0`, `host-1`, … by array index. */ + label?: string; +} + +/** + * How to handle the same interceptor `name` from more than one host after merge. + * - `error` (default): throw before the chain runs (SEP-global unique name). + * - `first-wins`: keep the first entry in host order; ignore later duplicates. + */ +export type DuplicateInterceptorNamePolicy = 'error' | 'first-wins'; + +export interface MergeInterceptorChainEntriesOptions { + duplicateNamePolicy?: DuplicateInterceptorNamePolicy; +} diff --git a/typescript/sdk/src/client/interceptor-chain-runner.test.ts b/typescript/sdk/src/client/interceptor-chain-runner.test.ts new file mode 100644 index 0000000..c46e3d5 --- /dev/null +++ b/typescript/sdk/src/client/interceptor-chain-runner.test.ts @@ -0,0 +1,104 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from '../protocol/constants.js'; +import { McpInterceptorValidationException } from '../protocol/errors.js'; +import { connectInterceptorHost } from '../__tests__/fixtures/hosts.js'; +import { InterceptorChainRunner } from './interceptor-chain-runner.js'; + +describe('InterceptorChainRunner', () => { + it('respects events filter via shouldIntercept', () => { + const runner = new InterceptorChainRunner({ + interceptorClients: [], + events: [InterceptionEvents.ToolsCall], + }); + expect(runner.shouldIntercept(InterceptionEvents.ToolsCall)).toBe(true); + expect(runner.shouldIntercept(InterceptionEvents.PromptsGet)).toBe(false); + }); + + it('runChainPhaseOrThrow throws validation exception in-process', async () => { + const host = await connectInterceptorHost([ + { + descriptor: { + name: 'blocker', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => + ({ + type: 'validation', + phase: 'request', + valid: false, + severity: 'error', + messages: [{ message: 'blocked', severity: 'error' }], + }) as const, + }, + ]); + + const runner = new InterceptorChainRunner({ + interceptorClients: [host.client], + }); + + const err = await runner + .runChainPhaseOrThrow('tools/call', InterceptionEvents.ToolsCall, 'request', {}) + .catch((e: unknown) => e); + expect(err).toBeInstanceOf(McpInterceptorValidationException); + expect((err as McpInterceptorValidationException).message).toMatch( + /blocker.*reported invalid/i, + ); + + await host.close(); + }); + + it('runs multiple interceptor clients in order', async () => { + const first = await connectInterceptorHost([ + { + descriptor: { + name: 'first', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + priorityHint: 0, + }, + handler: (params) => ({ + type: 'mutation', + phase: params.phase, + modified: true, + payload: { ...(params.payload as object), first: true }, + }), + }, + ]); + + const second = await connectInterceptorHost([ + { + descriptor: { + name: 'second', + type: 'mutation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + priorityHint: 0, + }, + handler: (params) => ({ + type: 'mutation', + phase: params.phase, + modified: true, + payload: { ...(params.payload as object), second: true }, + }), + }, + ]); + + const runner = new InterceptorChainRunner({ + interceptorClients: [first.client, second.client], + }); + + const payload = await runner.runChainPhaseOrThrow( + 'tools/call', + InterceptionEvents.ToolsCall, + 'request', + {}, + ); + + expect(payload).toMatchObject({ first: true, second: true }); + + await first.close(); + await second.close(); + }); +}); diff --git a/typescript/sdk/src/client/interceptor-chain-runner.ts b/typescript/sdk/src/client/interceptor-chain-runner.ts new file mode 100644 index 0000000..0806ef4 --- /dev/null +++ b/typescript/sdk/src/client/interceptor-chain-runner.ts @@ -0,0 +1,96 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { throwChainFailure } from '../protocol/errors.js'; +import type { + ExecuteChainRequestParams, + InterceptorChainResult, + InterceptorPhase, + InvokeInterceptorContext, +} from '../protocol/types.js'; +import { executeInterceptorChainOnClients } from './execute-interceptor-chain-on-clients.js'; +import type { DuplicateInterceptorNamePolicy } from './interceptor-chain-entry.js'; + +export interface InterceptorChainRunnerOptions { + interceptorClients: Client[]; + /** When set, only these lifecycle events are intercepted. */ + events?: string[]; + timeoutMs?: number; + defaultContext?: InvokeInterceptorContext; + /** Passed to multi-host chain merge when more than one interceptor client is configured. */ + duplicateNamePolicy?: DuplicateInterceptorNamePolicy; +} + +export class InterceptorChainRunner { + private readonly clients: Client[]; + private readonly events: string[] | undefined; + private readonly timeoutMs: number | undefined; + private readonly defaultContext: InvokeInterceptorContext | undefined; + private readonly duplicateNamePolicy: DuplicateInterceptorNamePolicy | undefined; + + constructor(options: InterceptorChainRunnerOptions) { + this.clients = options.interceptorClients; + this.events = options.events; + this.timeoutMs = options.timeoutMs; + this.defaultContext = options.defaultContext; + this.duplicateNamePolicy = options.duplicateNamePolicy; + } + + shouldIntercept(eventName: string): boolean { + if (!this.events || this.events.length === 0) { + return true; + } + return this.events.includes(eventName); + } + + async runChainPhase( + eventName: string, + phase: InterceptorPhase, + payload: unknown, + signal?: AbortSignal, + ): Promise<{ payload: unknown; chainResult: InterceptorChainResult }> { + const currentPayload = payload; + + const chainParams: ExecuteChainRequestParams = { + event: eventName, + phase, + payload: currentPayload, + timeoutMs: this.timeoutMs, + context: this.defaultContext, + }; + + const chainResult = await executeInterceptorChainOnClients( + this.clients.map((client, index) => ({ client, label: `host-${index}` })), + chainParams, + { signal, duplicateNamePolicy: this.duplicateNamePolicy }, + ); + + if (chainResult.status !== 'success') { + return { payload: currentPayload, chainResult }; + } + + return { + payload: chainResult.finalPayload ?? currentPayload, + chainResult, + }; + } + + async runChainPhaseOrThrow( + operation: string, + eventName: string, + phase: InterceptorPhase, + payload: unknown, + signal?: AbortSignal, + ): Promise { + const { payload: processed, chainResult } = await this.runChainPhase( + eventName, + phase, + payload, + signal, + ); + if (chainResult.status !== 'success') { + throwChainFailure(operation, phase, chainResult.status, chainResult); + } + return processed; + } +} diff --git a/typescript/sdk/src/client/matches-event.test.ts b/typescript/sdk/src/client/matches-event.test.ts new file mode 100644 index 0000000..19c6877 --- /dev/null +++ b/typescript/sdk/src/client/matches-event.test.ts @@ -0,0 +1,16 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from '../protocol/constants.js'; +import { matchesEvent } from './chain-orchestrator.js'; + +describe('matchesEvent', () => { + it('matches exact event names', () => { + expect(matchesEvent([InterceptionEvents.ToolsCall], InterceptionEvents.ToolsCall)).toBe(true); + expect(matchesEvent([InterceptionEvents.ToolsCall], InterceptionEvents.PromptsGet)).toBe(false); + }); + + it('matches wildcard *', () => { + expect(matchesEvent([InterceptionEvents.All], InterceptionEvents.ResourcesRead)).toBe(true); + }); +}); diff --git a/typescript/sdk/src/client/merge-interceptor-chain-entries.ts b/typescript/sdk/src/client/merge-interceptor-chain-entries.ts new file mode 100644 index 0000000..a7e53dd --- /dev/null +++ b/typescript/sdk/src/client/merge-interceptor-chain-entries.ts @@ -0,0 +1,95 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Interceptor } from '../protocol/types.js'; +import { DuplicateInterceptorNameError } from '../protocol/errors.js'; +import { listInterceptors } from './client-extensions.js'; +import type { + InterceptorChainEntry, + InterceptorChainHost, + MergeInterceptorChainEntriesOptions, +} from './interceptor-chain-entry.js'; + +function hostLabel(host: InterceptorChainHost, index: number): string { + return host.label?.trim() || `host-${index}`; +} + +/** + * Lists interceptors from each host and returns flat entries (no duplicate check). + */ +export async function listInterceptorChainEntries( + hosts: InterceptorChainHost[], + listParams?: { event?: string }, +): Promise { + const entries: InterceptorChainEntry[] = []; + + for (let i = 0; i < hosts.length; i++) { + const host = hosts[i]; + if (!host) { + continue; + } + const label = hostLabel(host, i); + const listed = await listInterceptors(host.client, listParams); + for (const descriptor of listed.interceptors) { + entries.push({ descriptor, client: host.client, hostLabel: label }); + } + } + + return entries; +} + +/** + * Applies duplicate-name policy after {@link listInterceptorChainEntries}. + */ +export function mergeInterceptorChainEntries( + entries: InterceptorChainEntry[], + options?: MergeInterceptorChainEntriesOptions, +): InterceptorChainEntry[] { + const policy = options?.duplicateNamePolicy ?? 'error'; + const byName = new Map(); + + for (const entry of entries) { + const name = entry.descriptor.name; + let group = byName.get(name); + if (!group) { + group = []; + byName.set(name, group); + } + group.push(entry); + } + + const conflicts: Array<{ name: string; hosts: string[] }> = []; + for (const [name, group] of byName) { + if (group.length > 1) { + conflicts.push({ name, hosts: group.map((e) => e.hostLabel) }); + } + } + + if (policy === 'error' && conflicts.length > 0) { + throw new DuplicateInterceptorNameError(conflicts); + } + + if (policy === 'first-wins') { + const merged: InterceptorChainEntry[] = []; + const seen = new Set(); + for (const entry of entries) { + if (seen.has(entry.descriptor.name)) { + continue; + } + seen.add(entry.descriptor.name); + merged.push(entry); + } + return merged; + } + + return entries; +} + +export function interceptorsFromEntries(entries: InterceptorChainEntry[]): Interceptor[] { + return entries.map((e) => e.descriptor); +} + +export function clientByInterceptorName( + entries: InterceptorChainEntry[], +): Map { + return new Map(entries.map((e) => [e.descriptor.name, e.client])); +} diff --git a/typescript/sdk/src/gateway/connect-interceptor-client.ts b/typescript/sdk/src/gateway/connect-interceptor-client.ts new file mode 100644 index 0000000..b0290b1 --- /dev/null +++ b/typescript/sdk/src/gateway/connect-interceptor-client.ts @@ -0,0 +1,16 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { McpInterceptorServerConnectionOptions } from './mcp-interceptor-server-connection-options.js'; + +export async function connectInterceptorClient( + connection: McpInterceptorServerConnectionOptions, + signal?: AbortSignal, +): Promise { + const client = new Client( + connection.clientInfo ?? { name: 'interceptor-client', version: '1.0.0' }, + connection.clientOptions ?? { capabilities: {} }, + ); + await client.connect(connection.transport, { signal }); + return client; +} diff --git a/typescript/sdk/src/gateway/gateway-interceptor-client-pool.ts b/typescript/sdk/src/gateway/gateway-interceptor-client-pool.ts new file mode 100644 index 0000000..6994686 --- /dev/null +++ b/typescript/sdk/src/gateway/gateway-interceptor-client-pool.ts @@ -0,0 +1,74 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { McpInterceptorServerConnectionOptions } from './mcp-interceptor-server-connection-options.js'; +import { connectInterceptorClient } from './connect-interceptor-client.js'; +import { GatewayResolvedInterceptorClients } from './gateway-resolved-interceptor-clients.js'; + +export class GatewayInterceptorClientPool { + private readonly cache = new Map>(); + + async resolveClients( + connections: McpInterceptorServerConnectionOptions[], + signal?: AbortSignal, + ): Promise { + if (connections.length === 0) { + return new GatewayResolvedInterceptorClients([]); + } + + const clients: Client[] = []; + const owned: Client[] = []; + + for (const connection of connections) { + if (!connection.transport) { + throw new Error('transport is required on interceptor server connection options'); + } + + const connectionId = connection.connectionId?.trim(); + if (!connectionId) { + const client = await connectInterceptorClient(connection, signal); + clients.push(client); + owned.push(client); + continue; + } + + let pending = this.cache.get(connectionId); + if (!pending) { + pending = connectInterceptorClient(connection, signal); + this.cache.set(connectionId, pending); + pending.catch(() => { + this.cache.delete(connectionId); + }); + } + + try { + clients.push(await pending); + } catch (error) { + this.cache.delete(connectionId); + throw error; + } + } + + return new GatewayResolvedInterceptorClients( + clients, + owned.map((client) => ({ + dispose: () => client.close(), + })), + ); + } + + async dispose(): Promise { + const pending = [...this.cache.values()]; + this.cache.clear(); + const clients = await Promise.all( + pending.map(async (p) => { + try { + return await p; + } catch { + return undefined; + } + }), + ); + await Promise.all(clients.filter((c): c is Client => c !== undefined).map((c) => c.close())); + } +} diff --git a/typescript/sdk/src/gateway/gateway-interceptor-client-provider.ts b/typescript/sdk/src/gateway/gateway-interceptor-client-provider.ts new file mode 100644 index 0000000..a685f99 --- /dev/null +++ b/typescript/sdk/src/gateway/gateway-interceptor-client-provider.ts @@ -0,0 +1,58 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { GatewayMessageContext } from './gateway-message-context.js'; +import type { McpInterceptorServerConnectionOptions } from './mcp-interceptor-server-connection-options.js'; +import { GatewayInterceptorClientPool } from './gateway-interceptor-client-pool.js'; +import { GatewayResolvedInterceptorClients } from './gateway-resolved-interceptor-clients.js'; + +export type InterceptorServerConnectionResolver = ( + context: GatewayMessageContext, + event: string, + signal?: AbortSignal, +) => Promise; + +export class GatewayInterceptorClientProvider { + private readonly staticClients: Client[]; + private readonly connectionResolver?: InterceptorServerConnectionResolver; + private readonly clientPool?: GatewayInterceptorClientPool; + + constructor(staticClients: Client[], connectionResolver?: InterceptorServerConnectionResolver) { + this.staticClients = staticClients; + this.connectionResolver = connectionResolver; + this.clientPool = connectionResolver ? new GatewayInterceptorClientPool() : undefined; + } + + async resolve( + messageContext: GatewayMessageContext, + event: string, + signal?: AbortSignal, + ): Promise { + if (!this.connectionResolver) { + return new GatewayResolvedInterceptorClients(this.staticClients); + } + + const resolvedConnections = (await this.connectionResolver(messageContext, event, signal)) ?? []; + const resolvedDynamic = await this.clientPool!.resolveClients(resolvedConnections, signal); + + if (this.staticClients.length === 0) { + return resolvedDynamic; + } + + if (resolvedDynamic.clients.length === 0) { + await resolvedDynamic.dispose(); + return new GatewayResolvedInterceptorClients(this.staticClients); + } + + return new GatewayResolvedInterceptorClients( + [...this.staticClients, ...resolvedDynamic.clients], + [resolvedDynamic], + ); + } + + async dispose(): Promise { + if (this.clientPool) { + await this.clientPool.dispose(); + } + } +} diff --git a/typescript/sdk/src/gateway/gateway-message-context.ts b/typescript/sdk/src/gateway/gateway-message-context.ts new file mode 100644 index 0000000..3a96d8a --- /dev/null +++ b/typescript/sdk/src/gateway/gateway-message-context.ts @@ -0,0 +1,11 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +/** Per-request context passed to {@link McpInterceptorGatewayOptions.interceptorServerConnectionResolver}. */ +export interface GatewayMessageContext { + /** JSON-RPC method for the proxied MCP request (e.g. `tools/call`). */ + method: string; + params?: unknown; + /** From MCP server `RequestHandlerExtra` when present. */ + authInfo?: unknown; + sessionId?: string; +} diff --git a/typescript/sdk/src/gateway/gateway-protocol-bridge.ts b/typescript/sdk/src/gateway/gateway-protocol-bridge.ts new file mode 100644 index 0000000..531c9ca --- /dev/null +++ b/typescript/sdk/src/gateway/gateway-protocol-bridge.ts @@ -0,0 +1,89 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import type { ServerCapabilities } from '@modelcontextprotocol/sdk/types.js'; +import { invokeInterceptor, listInterceptors } from '../client/client-extensions.js'; +import { + InvokeInterceptorRequestSchema, + ListInterceptorsRequestSchema, +} from '../protocol/zod-schemas.js'; +import type { + Interceptor, + InterceptorsCapability, + InvokeInterceptorRequestParams, + ListInterceptorsRequestParams, +} from '../protocol/types.js'; +function readInterceptorCapability(client: Client): InterceptorsCapability | undefined { + const caps = client.getServerCapabilities() as ServerCapabilities & { + interceptor?: InterceptorsCapability; + }; + return caps?.interceptor; +} + +export class GatewayInterceptorProtocolBridge { + private readonly interceptorClients: Client[]; + + constructor(interceptorClients: Client[]) { + this.interceptorClients = interceptorClients; + } + + configure(server: Server): void { + const allEvents = new Set(); + let anyCapability = false; + + for (const client of this.interceptorClients) { + const cap = readInterceptorCapability(client); + if (!cap) { + continue; + } + anyCapability = true; + for (const ev of cap.supportedEvents) { + allEvents.add(ev); + } + } + + if (anyCapability) { + server.registerCapabilities({ + interceptor: { + supportedEvents: [...allEvents], + }, + } as ServerCapabilities); + } + + server.setRequestHandler(ListInterceptorsRequestSchema, async (request) => { + const params = request.params as ListInterceptorsRequestParams | undefined; + const aggregated: Interceptor[] = []; + + for (const client of this.interceptorClients) { + const result = await listInterceptors(client, params); + aggregated.push(...result.interceptors); + } + + return { interceptors: aggregated }; + }); + + server.setRequestHandler(InvokeInterceptorRequestSchema, async (request) => { + const params = request.params as InvokeInterceptorRequestParams; + + for (const client of this.interceptorClients) { + try { + const result = await invokeInterceptor(client, params); + return result as unknown as Record; + } catch (err) { + const code = + typeof err === 'object' && + err !== null && + 'code' in err && + (err as { code: unknown }).code === -32602; + if (code) { + continue; + } + throw err; + } + } + + throw new Error(`Interceptor '${params.name}' not found on any interceptor server`); + }); + } +} diff --git a/typescript/sdk/src/gateway/gateway-proxy-configurator.ts b/typescript/sdk/src/gateway/gateway-proxy-configurator.ts new file mode 100644 index 0000000..76c0eba --- /dev/null +++ b/typescript/sdk/src/gateway/gateway-proxy-configurator.ts @@ -0,0 +1,270 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { + CallToolRequestSchema, + CompleteRequestSchema, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, + SetLevelRequestSchema, + SubscribeRequestSchema, + UnsubscribeRequestSchema, + type CallToolRequest, + type GetPromptRequest, + type Implementation, + type ListPromptsRequest, + type ListResourcesRequest, + type ListToolsRequest, + type ReadResourceRequest, + type ServerCapabilities, + type SubscribeRequest, +} from '@modelcontextprotocol/sdk/types.js'; +import { InterceptorChainRunner } from '../client/interceptor-chain-runner.js'; +import { InterceptionEvents } from '../protocol/constants.js'; +import type { InvokeInterceptorContext } from '../protocol/types.js'; +import type { GatewayInterceptorClientProvider } from './gateway-interceptor-client-provider.js'; +import type { GatewayMessageContext } from './gateway-message-context.js'; +import { runProxiedRequest } from './proxy-request.js'; + +export interface GatewayProxyConfiguratorOptions { + backendClient: Client; + interceptorClientProvider: GatewayInterceptorClientProvider; + events?: string[]; + timeoutMs?: number; + defaultContext?: InvokeInterceptorContext; + serverInfo?: Implementation; +} + +function cloneBackendCapabilities(capabilities: ServerCapabilities): ServerCapabilities { + const cloned = structuredClone(capabilities) as ServerCapabilities & { tasks?: unknown }; + delete cloned.tasks; + return cloned; +} + +function coerceParams( + request: T, + mutated: unknown, +): NonNullable { + if (typeof mutated === 'object' && mutated !== null) { + return mutated as NonNullable; + } + return request.params as NonNullable; +} + +function messageContextFromRequest( + method: string, + params: unknown, + extra: { authInfo?: unknown; sessionId?: string; signal?: AbortSignal }, +): GatewayMessageContext { + return { + method, + params, + authInfo: extra.authInfo, + sessionId: extra.sessionId, + }; +} + +export class GatewayProxyConfigurator { + private readonly backend: Client; + private readonly provider: GatewayInterceptorClientProvider; + private readonly events?: string[]; + private readonly timeoutMs?: number; + private readonly defaultContext?: InvokeInterceptorContext; + private readonly serverInfo?: Implementation; + + constructor(options: GatewayProxyConfiguratorOptions) { + this.backend = options.backendClient; + this.provider = options.interceptorClientProvider; + this.events = options.events; + this.timeoutMs = options.timeoutMs; + this.defaultContext = options.defaultContext; + this.serverInfo = options.serverInfo; + } + + configure(server: Server): void { + const backendCaps = this.backend.getServerCapabilities(); + + if (backendCaps) { + server.registerCapabilities(cloneBackendCapabilities(backendCaps)); + } + + if (this.serverInfo) { + // v1 Server info is fixed at construction; callers should pass the same override there. + } + + if (backendCaps?.tools) { + this.configureTools(server); + } + if (backendCaps?.prompts) { + this.configurePrompts(server); + } + if (backendCaps?.resources) { + this.configureResources(server, backendCaps); + } + if (backendCaps?.completions) { + server.setRequestHandler(CompleteRequestSchema, (request, extra) => + this.backend.complete(request.params, { signal: extra.signal }), + ); + } + if (backendCaps?.logging) { + server.setRequestHandler(SetLevelRequestSchema, async (request, extra) => { + await this.backend.setLoggingLevel(request.params.level, { signal: extra.signal }); + return {}; + }); + } + } + + private createChainRunner(interceptorClients: Client[]): InterceptorChainRunner { + return new InterceptorChainRunner({ + interceptorClients, + events: this.events, + timeoutMs: this.timeoutMs, + defaultContext: this.defaultContext, + }); + } + + private async runProxied( + context: GatewayMessageContext, + operation: string, + eventName: string, + requestParams: unknown, + forward: (mutatedParams: unknown, signal?: AbortSignal) => Promise, + signal?: AbortSignal, + ): Promise { + const resolved = await this.provider.resolve(context, eventName, signal); + try { + return await runProxiedRequest({ + operation, + eventName, + requestParams, + chainRunner: this.createChainRunner(resolved.clients), + signal, + forward, + }); + } finally { + await resolved.dispose(); + } + } + + private configureTools(server: Server): void { + server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => + this.runProxied( + messageContextFromRequest('tools/list', request.params ?? {}, extra), + 'tools/list', + InterceptionEvents.ToolsList, + request.params ?? {}, + (params, sig) => + this.backend.listTools(params as ListToolsRequest['params'], { signal: sig }), + extra.signal, + ), + ); + + server.setRequestHandler(CallToolRequestSchema, async (request, extra) => + this.runProxied( + messageContextFromRequest('tools/call', request.params, extra), + 'tools/call', + InterceptionEvents.ToolsCall, + request.params, + (params, sig) => + this.backend.callTool( + coerceParams(request, params) as CallToolRequest['params'], + undefined, + { signal: sig }, + ), + extra.signal, + ), + ); + } + + private configurePrompts(server: Server): void { + server.setRequestHandler(ListPromptsRequestSchema, async (request, extra) => + this.runProxied( + messageContextFromRequest('prompts/list', request.params ?? {}, extra), + 'prompts/list', + InterceptionEvents.PromptsList, + request.params ?? {}, + (params, sig) => + this.backend.listPrompts(params as ListPromptsRequest['params'], { signal: sig }), + extra.signal, + ), + ); + + server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => + this.runProxied( + messageContextFromRequest('prompts/get', request.params, extra), + 'prompts/get', + InterceptionEvents.PromptsGet, + request.params, + (params, sig) => + this.backend.getPrompt( + coerceParams(request, params) as GetPromptRequest['params'], + { signal: sig }, + ), + extra.signal, + ), + ); + } + + private configureResources(server: Server, backendCaps: ServerCapabilities): void { + server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => + this.runProxied( + messageContextFromRequest('resources/list', request.params ?? {}, extra), + 'resources/list', + InterceptionEvents.ResourcesList, + request.params ?? {}, + (params, sig) => + this.backend.listResources(params as ListResourcesRequest['params'], { signal: sig }), + extra.signal, + ), + ); + + server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => + this.runProxied( + messageContextFromRequest('resources/read', request.params, extra), + 'resources/read', + InterceptionEvents.ResourcesRead, + request.params, + (params, sig) => + this.backend.readResource( + coerceParams(request, params) as ReadResourceRequest['params'], + { signal: sig }, + ), + extra.signal, + ), + ); + + server.setRequestHandler(ListResourceTemplatesRequestSchema, (request, extra) => + this.backend.listResourceTemplates(request.params, { signal: extra.signal }), + ); + + if (backendCaps.resources?.subscribe) { + server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => { + await this.runProxied( + messageContextFromRequest('resources/subscribe', request.params, extra), + 'resources/subscribe', + InterceptionEvents.ResourcesSubscribe, + request.params, + async (params, sig) => { + await this.backend.subscribeResource( + coerceParams(request, params) as SubscribeRequest['params'], + { signal: sig }, + ); + return {}; + }, + extra.signal, + ); + return {}; + }); + + server.setRequestHandler(UnsubscribeRequestSchema, async (request, extra) => { + await this.backend.unsubscribeResource(request.params, { signal: extra.signal }); + return {}; + }); + } + } +} diff --git a/typescript/sdk/src/gateway/gateway-resolved-interceptor-clients.ts b/typescript/sdk/src/gateway/gateway-resolved-interceptor-clients.ts new file mode 100644 index 0000000..167691b --- /dev/null +++ b/typescript/sdk/src/gateway/gateway-resolved-interceptor-clients.ts @@ -0,0 +1,22 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; + +export class GatewayResolvedInterceptorClients { + readonly clients: Client[]; + private readonly ownedDisposables: Array<{ dispose(): Promise }>; + + constructor( + clients: Client[], + ownedDisposables: Array<{ dispose(): Promise }> = [], + ) { + this.clients = clients; + this.ownedDisposables = ownedDisposables; + } + + async dispose(): Promise { + for (const disposable of this.ownedDisposables) { + await disposable.dispose(); + } + } +} diff --git a/typescript/sdk/src/gateway/mcp-interceptor-gateway.ts b/typescript/sdk/src/gateway/mcp-interceptor-gateway.ts new file mode 100644 index 0000000..71806cb --- /dev/null +++ b/typescript/sdk/src/gateway/mcp-interceptor-gateway.ts @@ -0,0 +1,222 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { + PromptListChangedNotificationSchema, + ResourceListChangedNotificationSchema, + ToolListChangedNotificationSchema, + type Implementation, +} from '@modelcontextprotocol/sdk/types.js'; +import type { InvokeInterceptorContext } from '../protocol/types.js'; +import { connectInterceptorClient } from './connect-interceptor-client.js'; +import { + GatewayInterceptorClientProvider, + type InterceptorServerConnectionResolver, +} from './gateway-interceptor-client-provider.js'; +import { GatewayInterceptorProtocolBridge } from './gateway-protocol-bridge.js'; +import { GatewayProxyConfigurator } from './gateway-proxy-configurator.js'; +import type { McpInterceptorServerConnectionOptions } from './mcp-interceptor-server-connection-options.js'; + +export interface McpInterceptorGatewayOptions { + /** Connected client for the application (backend) MCP server. */ + backendClient: Client; + /** Pre-connected interceptor host clients, executed in order. */ + interceptorClients?: Client[]; + /** + * Outbound interceptor connections the gateway should open. + * Use {@link McpInterceptorGateway.createAsync} when this is set. + */ + interceptorServerConnections?: McpInterceptorServerConnectionOptions[]; + /** + * Per-request resolver for additional interceptor host connections (transparent proxy only). + * Not supported with {@link McpInterceptorGatewayOptions.exposeInterceptorProtocol}. + */ + interceptorServerConnectionResolver?: InterceptorServerConnectionResolver; + /** When set, only these lifecycle events run interceptor chains. */ + events?: string[]; + timeoutMs?: number; + defaultContext?: InvokeInterceptorContext; + /** + * When true, expose `interceptors/list` and `interceptor/invoke` on the proxy server + * (aggregated across static interceptor clients only). + */ + exposeInterceptorProtocol?: boolean; + /** + * Optional server identity for connecting clients. v1 `Server` info is set at construction; + * pass the same value to your `new Server(...)` call when overriding. + */ + serverInfo?: Implementation; +} + +function validateGatewayOptions(options: McpInterceptorGatewayOptions): void { + if (!options.backendClient) { + throw new Error('backendClient is required'); + } + + if (options.exposeInterceptorProtocol && options.interceptorServerConnectionResolver) { + throw new Error( + 'interceptorServerConnectionResolver is only supported for the transparent proxy path. ' + + 'Disable exposeInterceptorProtocol or use static interceptorClients for SEP passthrough.', + ); + } + + const hasStatic = (options.interceptorClients?.length ?? 0) > 0; + const hasConnections = (options.interceptorServerConnections?.length ?? 0) > 0; + const hasResolver = options.interceptorServerConnectionResolver !== undefined; + + if (!hasStatic && !hasConnections && !hasResolver) { + throw new Error( + 'At least one of interceptorClients, interceptorServerConnections, or interceptorServerConnectionResolver is required', + ); + } + + if (hasConnections) { + throw new Error( + 'Use McpInterceptorGateway.createAsync when interceptorServerConnections is configured', + ); + } +} + +/** + * Transparent MCP gateway: presents as the backend server while routing requests + * through interceptor host chains. + */ +export class McpInterceptorGateway { + readonly backendClient: Client; + readonly interceptorClients: Client[]; + private readonly interceptorClientProvider: GatewayInterceptorClientProvider; + private readonly proxyConfigurator: GatewayProxyConfigurator; + private readonly protocolBridge?: GatewayInterceptorProtocolBridge; + private readonly notificationCleanups: Array<() => void> = []; + private readonly ownedClients: Client[]; + + constructor(options: McpInterceptorGatewayOptions, ownedClients: Client[] = []) { + validateGatewayOptions(options); + + this.backendClient = options.backendClient; + this.interceptorClients = options.interceptorClients ?? []; + this.ownedClients = ownedClients; + + this.interceptorClientProvider = new GatewayInterceptorClientProvider( + this.interceptorClients, + options.interceptorServerConnectionResolver, + ); + + this.proxyConfigurator = new GatewayProxyConfigurator({ + backendClient: options.backendClient, + interceptorClientProvider: this.interceptorClientProvider, + events: options.events, + timeoutMs: options.timeoutMs, + defaultContext: options.defaultContext, + serverInfo: options.serverInfo, + }); + + if (options.exposeInterceptorProtocol) { + this.protocolBridge = new GatewayInterceptorProtocolBridge(this.interceptorClients); + } + } + + /** + * Creates a gateway and connects any configured external interceptor servers. + */ + static async createAsync( + options: McpInterceptorGatewayOptions, + signal?: AbortSignal, + ): Promise { + if (!options.backendClient) { + throw new Error('backendClient is required'); + } + + if (options.exposeInterceptorProtocol && options.interceptorServerConnectionResolver) { + throw new Error( + 'interceptorServerConnectionResolver is only supported for the transparent proxy path. ' + + 'Disable exposeInterceptorProtocol or use static interceptorClients for SEP passthrough.', + ); + } + + const ownedClients: Client[] = []; + const interceptorClients: Client[] = [...(options.interceptorClients ?? [])]; + + try { + if (options.interceptorServerConnections?.length) { + for (const connection of options.interceptorServerConnections) { + const client = await connectInterceptorClient(connection, signal); + interceptorClients.push(client); + ownedClients.push(client); + } + } + + const hasStatic = interceptorClients.length > 0; + const hasResolver = options.interceptorServerConnectionResolver !== undefined; + if (!hasStatic && !hasResolver) { + throw new Error( + 'At least one of interceptorClients, interceptorServerConnections, or interceptorServerConnectionResolver is required', + ); + } + + return new McpInterceptorGateway( + { + ...options, + interceptorClients, + interceptorServerConnections: undefined, + }, + ownedClients, + ); + } catch (error) { + await Promise.all(ownedClients.map((c) => c.close())); + throw error; + } + } + + /** Wire proxy handlers and mirror backend capabilities. Call before `server.connect()`. */ + configureServer(server: Server): void { + this.proxyConfigurator.configure(server); + this.protocolBridge?.configure(server); + } + + /** Forward backend list-changed notifications to clients connected to the proxy server. */ + registerNotificationForwarding(proxyServer: Server): void { + const backendCaps = this.backendClient.getServerCapabilities(); + + if (backendCaps?.tools?.listChanged) { + this.backendClient.setNotificationHandler(ToolListChangedNotificationSchema, async () => { + await proxyServer.sendToolListChanged(); + }); + this.notificationCleanups.push(() => + this.backendClient.removeNotificationHandler('notifications/tools/list_changed'), + ); + } + + if (backendCaps?.prompts?.listChanged) { + this.backendClient.setNotificationHandler(PromptListChangedNotificationSchema, async () => { + await proxyServer.sendPromptListChanged(); + }); + this.notificationCleanups.push(() => + this.backendClient.removeNotificationHandler('notifications/prompts/list_changed'), + ); + } + + if (backendCaps?.resources?.listChanged) { + this.backendClient.setNotificationHandler(ResourceListChangedNotificationSchema, async () => { + await proxyServer.sendResourceListChanged(); + }); + this.notificationCleanups.push(() => + this.backendClient.removeNotificationHandler('notifications/resources/list_changed'), + ); + } + } + + disposeNotificationForwarding(): void { + for (const cleanup of this.notificationCleanups.splice(0)) { + cleanup(); + } + } + + /** Closes connections opened by {@link McpInterceptorGateway.createAsync} and resolver pools. */ + async dispose(): Promise { + this.disposeNotificationForwarding(); + await Promise.all(this.ownedClients.splice(0).map((c) => c.close())); + await this.interceptorClientProvider.dispose(); + } +} diff --git a/typescript/sdk/src/gateway/mcp-interceptor-server-connection-options.ts b/typescript/sdk/src/gateway/mcp-interceptor-server-connection-options.ts new file mode 100644 index 0000000..b87f05e --- /dev/null +++ b/typescript/sdk/src/gateway/mcp-interceptor-server-connection-options.ts @@ -0,0 +1,18 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { Implementation } from '@modelcontextprotocol/sdk/types.js'; + +export type InterceptorClientTransport = Parameters[0]; + +/** Options for connecting the gateway to an external interceptor host. */ +export interface McpInterceptorServerConnectionOptions { + /** + * Stable id for reusing a connected client across requests. + * When omitted, a client is created and closed after each resolution. + */ + connectionId?: string; + transport: InterceptorClientTransport; + clientInfo?: Implementation; + clientOptions?: ConstructorParameters[1]; +} diff --git a/typescript/sdk/src/gateway/proxy-request.ts b/typescript/sdk/src/gateway/proxy-request.ts new file mode 100644 index 0000000..cb3966e --- /dev/null +++ b/typescript/sdk/src/gateway/proxy-request.ts @@ -0,0 +1,43 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { InterceptorChainRunner } from '../client/interceptor-chain-runner.js'; + +export interface ProxiedRequestOptions { + operation: string; + eventName: string; + requestParams: unknown; + forward: (mutatedParams: unknown, signal?: AbortSignal) => Promise; + chainRunner: InterceptorChainRunner; + signal?: AbortSignal; +} + +/** Run request/response interceptor phases around a backend forward call. */ +export async function runProxiedRequest( + options: ProxiedRequestOptions, +): Promise { + let requestPayload = options.requestParams; + + if (options.chainRunner.shouldIntercept(options.eventName)) { + requestPayload = await options.chainRunner.runChainPhaseOrThrow( + options.operation, + options.eventName, + 'request', + requestPayload, + options.signal, + ); + } + + let result: T = await options.forward(requestPayload, options.signal); + + if (options.chainRunner.shouldIntercept(options.eventName)) { + result = (await options.chainRunner.runChainPhaseOrThrow( + options.operation, + options.eventName, + 'response', + result, + options.signal, + )) as T; + } + + return result; +} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 07d70db..b68cec8 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -2,11 +2,150 @@ // Use of this source code is governed by a Apache-2.0 // license that can be found in the LICENSE file. -/** - * MCP Interceptors TypeScript SDK - * - * Implements MCP Interceptors based on SEP-1763: - * https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763 - */ - -export * from './interceptors.js'; \ No newline at end of file +export { + InterceptorRequestMethods, + InterceptionEvents, +} from './protocol/constants.js'; + +export type { + ChainAbortInfo, + ChainValidationSummary, + ExecuteChainRequestParams, + Interceptor, + InterceptorChainResult, + InterceptorChainStatus, + InterceptorCompatibility, + InterceptorHook, + InterceptorMode, + PriorityHint, + PriorityHintByPhase, + InterceptorPhase, + InterceptorPrincipal, + InterceptorResult, + InterceptorType, + InterceptorsCapability, + InvokeInterceptorContext, + InvokeInterceptorRequestParams, + ListInterceptorsRequestParams, + ListInterceptorsResult, + MutationInterceptorResult, + SinkInterceptorResult, + ValidationInterceptorResult, + ValidationMessage, + ValidationSeverity, + ValidationSuggestion, +} from './protocol/types.js'; + +export { + isMutationResult, + isSinkResult, + isValidationResult, + parseInterceptorResult, + validationFailure, + validationSuccess, +} from './protocol/results.js'; + +export { + InterceptorHookSchema, + InterceptorResultSchema, + InterceptorSchema, + ListInterceptorsResultSchema, + MutationInterceptorResultSchema, + SinkInterceptorResultSchema, + ValidationInterceptorResultSchema, +} from './protocol/zod-schemas.js'; + +export { + executeInterceptorChain, + matchesEvent, + type InterceptorInvoker, +} from './client/chain-orchestrator.js'; + +export { resolvePriority } from './protocol/resolve-priority.js'; + +export { + executeInterceptorChainOnClient, + invokeInterceptor, + listInterceptors, +} from './client/client-extensions.js'; + +export { executeInterceptorChainOnClients } from './client/execute-interceptor-chain-on-clients.js'; +export type { ExecuteInterceptorChainOnClientsOptions } from './client/execute-interceptor-chain-on-clients.js'; + +export { + listInterceptorChainEntries, + mergeInterceptorChainEntries, + interceptorsFromEntries, + clientByInterceptorName, +} from './client/merge-interceptor-chain-entries.js'; + +export type { + InterceptorChainEntry, + InterceptorChainHost, + DuplicateInterceptorNamePolicy, + MergeInterceptorChainEntriesOptions, +} from './client/interceptor-chain-entry.js'; + +export { InterceptorChainRunner, type InterceptorChainRunnerOptions } from './client/interceptor-chain-runner.js'; + +export { + InterceptingMcpClient, + type InterceptingMcpClientOptions, +} from './client/intercepting-client.js'; + +export { + collectValidationErrorMessages, + DuplicateInterceptorNameError, + formatValidationFailedChainMessage, + McpInterceptorChainException, + McpInterceptorValidationException, + throwChainFailure, +} from './protocol/errors.js'; + +export { + buildInterceptorsCapability, + collectSupportedEvents, + registerInterceptorCapabilities, +} from './server/capabilities.js'; + +export { + registerInterceptorsOnServer, + type InterceptorHandler, + type RegisteredInterceptor, + type RegisterInterceptorsOptions, +} from './server/register-interceptors.js'; + +export { + buildInterceptorDescriptor, + type InterceptorDefinitionOptions, + type InterceptorPhaseOption, +} from './server/interceptor-definition.js'; + +export { + defineInterceptor, + invokeHandlerFunction, + type InterceptorHandlerFn, +} from './server/reflection.js'; + +export { + McpInterceptorGateway, + type McpInterceptorGatewayOptions, +} from './gateway/mcp-interceptor-gateway.js'; + +export type { McpInterceptorServerConnectionOptions } from './gateway/mcp-interceptor-server-connection-options.js'; +export type { GatewayMessageContext } from './gateway/gateway-message-context.js'; +export type { InterceptorServerConnectionResolver } from './gateway/gateway-interceptor-client-provider.js'; + +export { GatewayProxyConfigurator, type GatewayProxyConfiguratorOptions } from './gateway/gateway-proxy-configurator.js'; + +export type { + LlmCompletionRequestPayload, + LlmCompletionResponsePayload, + LlmMessage, + LlmUsage, +} from './protocol/llm-payload.js'; + +export { + InvokeInterceptorRequestSchema, + ListInterceptorsRequestSchema, +} from './protocol/zod-schemas.js'; diff --git a/typescript/sdk/src/interceptors.test.ts b/typescript/sdk/src/interceptors.test.ts deleted file mode 100644 index 67f270a..0000000 --- a/typescript/sdk/src/interceptors.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2025 The MCP Interceptors Authors. All rights reserved. -// Use of this source code is governed by a Apache-2.0 -// license that can be found in the LICENSE file. - -import { describe, it, expect } from 'vitest'; - -describe('Interceptors', () => { - it('should be defined', () => { - expect(true).toBe(true); - }); -}); \ No newline at end of file diff --git a/typescript/sdk/src/interceptors.ts b/typescript/sdk/src/interceptors.ts deleted file mode 100644 index 5568db2..0000000 --- a/typescript/sdk/src/interceptors.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2025 The MCP Interceptors Authors. All rights reserved. -// Use of this source code is governed by a Apache-2.0 -// license that can be found in the LICENSE file. - -/** - * Core interceptor functionality for MCP - * - * Based on SEP-1763: - * https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763 - */ - -// Placeholder for interceptor implementation \ No newline at end of file diff --git a/typescript/sdk/src/protocol/constants.ts b/typescript/sdk/src/protocol/constants.ts new file mode 100644 index 0000000..0848578 --- /dev/null +++ b/typescript/sdk/src/protocol/constants.ts @@ -0,0 +1,25 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. +// Use of this source code is governed by a Apache-2.0 +// license that can be found in the LICENSE file. + +/** JSON-RPC method names for the interceptors extension. */ +export const InterceptorRequestMethods = { + InterceptorsList: 'interceptors/list', + InterceptorInvoke: 'interceptor/invoke', +} as const; + +/** Well-known lifecycle event identifiers (SEP-1763). */ +export const InterceptionEvents = { + ToolsList: 'tools/list', + ToolsCall: 'tools/call', + PromptsList: 'prompts/list', + PromptsGet: 'prompts/get', + ResourcesList: 'resources/list', + ResourcesRead: 'resources/read', + ResourcesSubscribe: 'resources/subscribe', + SamplingCreateMessage: 'sampling/createMessage', + ElicitationCreate: 'elicitation/create', + RootsList: 'roots/list', + LlmCompletion: 'llm/completion', + All: '*', +} as const; diff --git a/typescript/sdk/src/protocol/errors.test.ts b/typescript/sdk/src/protocol/errors.test.ts new file mode 100644 index 0000000..0eb88fd --- /dev/null +++ b/typescript/sdk/src/protocol/errors.test.ts @@ -0,0 +1,116 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { + McpInterceptorChainException, + McpInterceptorValidationException, + collectValidationErrorMessages, + formatValidationFailedChainMessage, + throwChainFailure, +} from './errors.js'; +import type { InterceptorChainResult } from './types.js'; + +function validationFailedResult( + overrides: Partial = {}, +): InterceptorChainResult { + return { + status: 'validation_failed', + event: 'tools/call', + phase: 'request', + results: [ + { + type: 'validation', + phase: 'request', + interceptor: 'pii-validator', + valid: false, + severity: 'error', + messages: [ + { + path: '$.arguments', + message: 'Payload may contain Social Security Number data', + severity: 'error', + }, + ], + }, + ], + validationSummary: { errors: 1, warnings: 0, infos: 0 }, + totalDurationMs: 1, + abortedAt: { + interceptor: 'pii-validator', + reason: 'Payload may contain Social Security Number data', + type: 'validation', + }, + ...overrides, + }; +} + +describe('formatValidationFailedChainMessage', () => { + it('names the interceptor and reported-invalid outcome', () => { + const chainResult = validationFailedResult(); + const message = formatValidationFailedChainMessage('tools/call', chainResult); + expect(message).toContain('tools/call'); + expect(message).toContain('request phase'); + expect(message).toContain('validation interceptor "pii-validator" reported invalid'); + expect(message).toContain('Payload may contain Social Security Number data'); + expect(message).not.toMatch(/validation failed for tools\/call/i); + }); +}); + +describe('collectValidationErrorMessages', () => { + it('collects messages from invalid validation results', () => { + const messages = collectValidationErrorMessages(validationFailedResult()); + expect(messages).toHaveLength(1); + expect(messages[0]?.message).toContain('Social Security'); + }); +}); + +describe('throwChainFailure', () => { + it('throws McpInterceptorValidationException with SEP-aligned message', () => { + const chainResult = validationFailedResult(); + expect(() => throwChainFailure('tools/call', 'request', 'validation_failed', chainResult)).toThrow( + McpInterceptorValidationException, + ); + + try { + throwChainFailure('tools/call', 'request', 'validation_failed', chainResult); + } catch (err) { + const ex = err as McpInterceptorValidationException; + expect(ex.message).toContain('pii-validator'); + expect(ex.message).toContain('reported invalid'); + expect(ex.message).toMatch(/validation_failed|reported invalid/); + expect(ex.message).not.toMatch(/validation failed for tools\/call/i); + expect(ex.validationMessages).toHaveLength(1); + expect(ex.chainResult?.status).toBe('validation_failed'); + expect(ex.chainResult?.abortedAt?.type).toBe('validation'); + } + }); + + it('throws McpInterceptorChainException for mutation_failed with interceptor context', () => { + const chainResult: InterceptorChainResult = { + status: 'mutation_failed', + event: 'tools/call', + phase: 'response', + results: [], + validationSummary: { errors: 0, warnings: 0, infos: 0 }, + totalDurationMs: 1, + abortedAt: { + interceptor: 'bad-mutator', + reason: 'boom', + type: 'mutation', + }, + }; + + expect(() => throwChainFailure('tools/call', 'response', 'mutation_failed', chainResult)).toThrow( + McpInterceptorChainException, + ); + + try { + throwChainFailure('tools/call', 'response', 'mutation_failed', chainResult); + } catch (err) { + const ex = err as McpInterceptorChainException; + expect(ex.status).toBe('mutation_failed'); + expect(ex.message).toContain('bad-mutator'); + expect(ex.message).toContain('mutation interceptor'); + } + }); +}); diff --git a/typescript/sdk/src/protocol/errors.ts b/typescript/sdk/src/protocol/errors.ts new file mode 100644 index 0000000..2fe03ea --- /dev/null +++ b/typescript/sdk/src/protocol/errors.ts @@ -0,0 +1,158 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { isValidationResult } from './results.js'; +import type { + InterceptorChainResult, + InterceptorChainStatus, + InterceptorPhase, + ValidationMessage, +} from './types.js'; + +export class McpInterceptorValidationException extends Error { + readonly validationMessages: readonly ValidationMessage[]; + readonly chainResult?: InterceptorChainResult; + + constructor( + message: string, + validationMessages: readonly ValidationMessage[] = [], + chainResult?: InterceptorChainResult, + ) { + super(message); + this.name = 'McpInterceptorValidationException'; + this.validationMessages = validationMessages; + this.chainResult = chainResult; + } +} + +export class McpInterceptorChainException extends Error { + readonly operation: string; + readonly phase: InterceptorPhase; + readonly status: InterceptorChainStatus; + + constructor(message: string, operation: string, phase: InterceptorPhase, status: InterceptorChainStatus) { + super(message); + this.name = 'McpInterceptorChainException'; + this.operation = operation; + this.phase = phase; + this.status = status; + } +} + +export class DuplicateInterceptorNameError extends Error { + readonly conflicts: ReadonlyArray<{ name: string; hosts: readonly string[] }>; + + constructor(conflicts: Array<{ name: string; hosts: string[] }>) { + const detail = conflicts + .map((c) => `'${c.name}' on ${c.hosts.map((h) => `'${h}'`).join(', ')}`) + .join('; '); + super( + `Duplicate interceptor name(s) across hosts: ${detail}. ` + + 'Use globally unique interceptor names or duplicateNamePolicy: "first-wins".', + ); + this.name = 'DuplicateInterceptorNameError'; + this.conflicts = conflicts; + } +} + +/** Validation messages from chain results where the interceptor reported `valid: false`. */ +export function collectValidationErrorMessages( + chainResult: InterceptorChainResult, +): ValidationMessage[] { + const messages: ValidationMessage[] = []; + for (const result of chainResult.results) { + if (isValidationResult(result) && !result.valid && result.messages) { + messages.push(...result.messages); + } + } + return messages; +} + +function phaseLabel(phase: InterceptorPhase): string { + return `${phase} phase`; +} + +function eventLabel(operation: string, chainResult?: InterceptorChainResult): string { + return chainResult?.event ?? operation; +} + +/** + * SEP-aligned message: validator ran, reported invalid, chain status `validation_failed`. + */ +export function formatValidationFailedChainMessage( + operation: string, + chainResult: InterceptorChainResult, +): string { + const event = eventLabel(operation, chainResult); + const phase = chainResult.phase; + const messages = collectValidationErrorMessages(chainResult); + const aborted = chainResult.abortedAt; + + const interceptorName = + aborted?.type === 'validation' + ? aborted.interceptor + : chainResult.results.find((r) => isValidationResult(r) && !r.valid)?.interceptor; + + const nameClause = interceptorName + ? `validation interceptor "${interceptorName}" reported invalid` + : 'validation interceptor reported invalid'; + + const policyReason = messages.map((m) => m.message).filter(Boolean).join('; '); + const reason = + policyReason || + (aborted?.type === 'validation' ? aborted.reason : undefined) || + ''; + + const detail = reason ? ` — ${reason}` : ''; + + return `${event} (${phaseLabel(phase)}): ${nameClause}${detail}`; +} + +function formatMutationFailedChainMessage( + operation: string, + phase: InterceptorPhase, + chainResult?: InterceptorChainResult, +): string { + const event = eventLabel(operation, chainResult); + const aborted = chainResult?.abortedAt; + if (aborted?.type === 'mutation') { + return `${event} (${phaseLabel(phase)}): mutation interceptor "${aborted.interceptor}" failed — ${aborted.reason}`; + } + return `${event} (${phaseLabel(phase)}): interceptor chain aborted with status 'mutation_failed'.`; +} + +function formatTimeoutChainMessage( + operation: string, + phase: InterceptorPhase, + chainResult?: InterceptorChainResult, +): string { + const event = eventLabel(operation, chainResult); + const aborted = chainResult?.abortedAt; + if (aborted?.type === 'timeout') { + return `${event} (${phaseLabel(phase)}): interceptor chain timed out at "${aborted.interceptor}" — ${aborted.reason}`; + } + return `${event} (${phaseLabel(phase)}): interceptor chain timed out.`; +} + +export function throwChainFailure( + operation: string, + phase: InterceptorPhase, + status: InterceptorChainStatus, + chainResult?: InterceptorChainResult, +): never { + if (status === 'validation_failed') { + const messages = chainResult ? collectValidationErrorMessages(chainResult) : []; + const message = chainResult + ? formatValidationFailedChainMessage(operation, { ...chainResult, phase }) + : `${operation} (${phaseLabel(phase)}): validation interceptor reported invalid — chain status validation_failed`; + throw new McpInterceptorValidationException(message, messages, chainResult); + } + + const message = + status === 'mutation_failed' + ? formatMutationFailedChainMessage(operation, phase, chainResult) + : status === 'timeout' + ? formatTimeoutChainMessage(operation, phase, chainResult) + : `${eventLabel(operation, chainResult)} (${phaseLabel(phase)}): interceptor chain aborted with status '${status}'.`; + + throw new McpInterceptorChainException(message, operation, phase, status); +} diff --git a/typescript/sdk/src/protocol/llm-payload.ts b/typescript/sdk/src/protocol/llm-payload.ts new file mode 100644 index 0000000..ebbe39d --- /dev/null +++ b/typescript/sdk/src/protocol/llm-payload.ts @@ -0,0 +1,30 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +/** Request payload for the `llm/completion` lifecycle event. */ +export interface LlmCompletionRequestPayload { + model?: string; + messages?: LlmMessage[]; + maxTokens?: number; + temperature?: number; + metadata?: Record; +} + +/** Response payload for the `llm/completion` lifecycle event. */ +export interface LlmCompletionResponsePayload { + model?: string; + message?: LlmMessage; + stopReason?: string; + usage?: LlmUsage; + metadata?: Record; +} + +export interface LlmMessage { + role: string; + content: string; +} + +export interface LlmUsage { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; +} diff --git a/typescript/sdk/src/protocol/protocol-serialization.test.ts b/typescript/sdk/src/protocol/protocol-serialization.test.ts new file mode 100644 index 0000000..5634e28 --- /dev/null +++ b/typescript/sdk/src/protocol/protocol-serialization.test.ts @@ -0,0 +1,148 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from './constants.js'; +import { + InterceptorResultSchema, + InterceptorSchema, + ListInterceptorsResultSchema, + MutationInterceptorResultSchema, + SinkInterceptorResultSchema, + ValidationInterceptorResultSchema, +} from './zod-schemas.js'; +import type { Interceptor, InterceptorChainResult } from './types.js'; + +function roundTrip(schema: { parse: (v: unknown) => T }, value: T): T { + return schema.parse(JSON.parse(JSON.stringify(value))); +} + +describe('protocol wire shapes (Zod round-trip)', () => { + it('Interceptor round-trips rich descriptor', () => { + const interceptor: Interceptor = { + name: 'pii-validator', + version: '1.0.0', + description: 'Validates PII', + type: 'validation', + hooks: [ + { events: [InterceptionEvents.ToolsCall], phase: 'request' }, + { events: [InterceptionEvents.ToolsCall], phase: 'response' }, + ], + priorityHint: -1000, + compat: { minProtocol: '2024-11-05' }, + }; + + const parsed = roundTrip(InterceptorSchema, interceptor); + expect(parsed.name).toBe('pii-validator'); + expect(parsed.hooks).toHaveLength(2); + expect(parsed.priorityHint).toBe(-1000); + }); + + it('Interceptor round-trips per-phase priorityHint object', () => { + const parsed = InterceptorSchema.parse({ + name: 'phase-priority', + type: 'mutation', + hooks: [{ events: ['tools/call'], phase: 'request' }], + priorityHint: { request: -1000, response: 1000 }, + }); + expect(parsed.priorityHint).toEqual({ request: -1000, response: 1000 }); + }); + + it('normalizes C# mode "active" to enforce when parsing Interceptor', () => { + const parsed = InterceptorSchema.parse({ + name: 'from-csharp', + type: 'validation', + hooks: [{ events: ['tools/call'], phase: 'request' }], + mode: 'active', + }); + expect(parsed.mode).toBe('enforce'); + }); + + it('Interceptor omits unset optional fields in JSON', () => { + const minimal: Interceptor = { + name: 'test', + type: 'sink', + hooks: [{ events: [InterceptionEvents.All], phase: 'request' }], + }; + const json = JSON.stringify(minimal); + const doc = JSON.parse(json) as Record; + expect(doc.version).toBeUndefined(); + expect(doc.mode).toBeUndefined(); + expect(doc.failOpen).toBeUndefined(); + }); + + it('validation result round-trips', () => { + const parsed = roundTrip(ValidationInterceptorResultSchema, { + type: 'validation', + phase: 'request', + interceptor: 'val', + valid: false, + severity: 'error', + messages: [{ message: 'bad', severity: 'error', path: '$.x' }], + }); + expect(parsed.valid).toBe(false); + expect(parsed.messages?.[0]?.path).toBe('$.x'); + }); + + it('mutation and sink results round-trip', () => { + const mutation = roundTrip(MutationInterceptorResultSchema, { + type: 'mutation', + phase: 'request', + modified: true, + payload: { email: '[REDACTED]' }, + }); + expect(mutation.modified).toBe(true); + + const sink = roundTrip(SinkInterceptorResultSchema, { + type: 'sink', + phase: 'response', + recorded: true, + metrics: { latencyMs: 12.5 }, + }); + expect(sink.metrics?.latencyMs).toBe(12.5); + }); + + it('list interceptors result round-trips', () => { + const parsed = roundTrip(ListInterceptorsResultSchema, { + interceptors: [ + { + name: 'a', + type: 'validation', + hooks: [{ events: ['tools/call'], phase: 'request' }], + }, + ], + }); + expect(parsed.interceptors).toHaveLength(1); + }); + + it('discriminated union parses each interceptor result type', () => { + const validation = InterceptorResultSchema.parse({ + type: 'validation', + phase: 'request', + valid: true, + }); + expect(validation.type).toBe('validation'); + + const mutation = InterceptorResultSchema.parse({ + type: 'mutation', + phase: 'response', + modified: false, + }); + expect(mutation.type).toBe('mutation'); + }); + + it('chain status serializes as snake_case strings', () => { + const sample: InterceptorChainResult = { + status: 'validation_failed', + event: InterceptionEvents.ToolsCall, + phase: 'request', + results: [], + finalPayload: {}, + validationSummary: { errors: 1, warnings: 0, infos: 0 }, + totalDurationMs: 1, + }; + const json = JSON.stringify(sample); + expect(json).toContain('"validation_failed"'); + const parsed = JSON.parse(json) as { status: string }; + expect(parsed.status).toBe('validation_failed'); + }); +}); diff --git a/typescript/sdk/src/protocol/protocol.test.ts b/typescript/sdk/src/protocol/protocol.test.ts new file mode 100644 index 0000000..4f7120c --- /dev/null +++ b/typescript/sdk/src/protocol/protocol.test.ts @@ -0,0 +1,69 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from './constants.js'; +import { parseInterceptorResult } from './results.js'; +import type { Interceptor } from './types.js'; + +describe('parseInterceptorResult', () => { + it('parses validation results', () => { + const parsed = parseInterceptorResult({ + type: 'validation', + phase: 'request', + interceptor: 'pii-validator', + valid: false, + severity: 'error', + messages: [{ message: 'Contains PII', severity: 'error', path: '$.email' }], + }); + + expect(parsed.type).toBe('validation'); + if (parsed.type === 'validation') { + expect(parsed.valid).toBe(false); + expect(parsed.interceptor).toBe('pii-validator'); + expect(parsed.messages?.[0]?.path).toBe('$.email'); + } + }); + + it('parses mutation and sink results', () => { + const mutation = parseInterceptorResult({ + type: 'mutation', + phase: 'request', + modified: true, + payload: { email: '[REDACTED]' }, + }); + expect(mutation.type).toBe('mutation'); + + const sink = parseInterceptorResult({ + type: 'sink', + phase: 'response', + recorded: true, + metrics: { latencyMs: 12.5 }, + }); + expect(sink.type).toBe('sink'); + if (sink.type === 'sink') { + expect(sink.metrics?.latencyMs).toBe(12.5); + } + }); +}); + +describe('interceptor descriptor JSON shape', () => { + it('round-trips minimal fields', () => { + const interceptor: Interceptor = { + name: 'pii-validator', + type: 'validation', + hooks: [ + { + events: [InterceptionEvents.ToolsCall], + phase: 'request', + }, + ], + }; + + const json = JSON.stringify(interceptor); + const parsed = JSON.parse(json) as Interceptor; + + expect(parsed.name).toBe('pii-validator'); + expect(parsed.hooks[0]?.events).toContain(InterceptionEvents.ToolsCall); + expect(json).not.toContain('version'); + }); +}); diff --git a/typescript/sdk/src/protocol/resolve-priority.ts b/typescript/sdk/src/protocol/resolve-priority.ts new file mode 100644 index 0000000..0ad748e --- /dev/null +++ b/typescript/sdk/src/protocol/resolve-priority.ts @@ -0,0 +1,21 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Interceptor, InterceptorPhase } from './types.js'; + +/** + * Resolves mutation `priorityHint` for the chain phase (SEP priority resolution). + * Validation interceptors ignore priority; only call this when sorting mutations. + */ +export function resolvePriority( + interceptor: Pick, + phase: InterceptorPhase, +): number { + const hint = interceptor.priorityHint; + if (hint === undefined) { + return 0; + } + if (typeof hint === 'number') { + return hint; + } + return hint[phase] ?? 0; +} diff --git a/typescript/sdk/src/protocol/results.ts b/typescript/sdk/src/protocol/results.ts new file mode 100644 index 0000000..ae6fa5b --- /dev/null +++ b/typescript/sdk/src/protocol/results.ts @@ -0,0 +1,149 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. +// Use of this source code is governed by a Apache-2.0 +// license that can be found in the LICENSE file. + +import type { + InterceptorPhase, + InterceptorResult, + MutationInterceptorResult, + SinkInterceptorResult, + ValidationInterceptorResult, + ValidationMessage, + ValidationSeverity, +} from './types.js'; + +export function validationSuccess(phase: InterceptorPhase): ValidationInterceptorResult { + return { type: 'validation', phase, valid: true }; +} + +export function validationFailure( + phase: InterceptorPhase, + ...messages: ValidationMessage[] +): ValidationInterceptorResult { + return { + type: 'validation', + phase, + valid: false, + severity: 'error', + messages, + }; +} + +export function isValidationResult(r: InterceptorResult): r is ValidationInterceptorResult { + return r.type === 'validation'; +} + +export function isMutationResult(r: InterceptorResult): r is MutationInterceptorResult { + return r.type === 'mutation'; +} + +export function isSinkResult(r: InterceptorResult): r is SinkInterceptorResult { + return r.type === 'sink'; +} + +/** Parse a wire JSON value into a discriminated interceptor result. */ +export function parseInterceptorResult(value: unknown): InterceptorResult { + if (typeof value !== 'object' || value === null) { + throw new Error('Interceptor result must be an object'); + } + const obj = value as Record; + const type = obj.type; + if (obj.phase !== 'request' && obj.phase !== 'response') { + throw new Error(`Invalid interceptor result phase: ${String(obj.phase)}`); + } + const phase: InterceptorPhase = obj.phase; + + const base = { + interceptor: typeof obj.interceptor === 'string' ? obj.interceptor : undefined, + phase, + durationMs: typeof obj.durationMs === 'number' ? obj.durationMs : undefined, + info: + typeof obj.info === 'object' && obj.info !== null + ? (obj.info as Record) + : undefined, + }; + + switch (type) { + case 'validation': + return { + ...base, + type: 'validation', + valid: Boolean(obj.valid), + severity: parseSeverity(obj.severity), + messages: parseMessages(obj.messages), + suggestions: parseSuggestions(obj.suggestions), + }; + case 'mutation': + return { + ...base, + type: 'mutation', + modified: Boolean(obj.modified), + payload: obj.payload, + }; + case 'sink': + return { + ...base, + type: 'sink', + recorded: Boolean(obj.recorded), + metrics: parseMetrics(obj.metrics), + }; + default: + throw new Error(`Unknown interceptor result type: ${String(type)}`); + } +} + +function parseSeverity(value: unknown): ValidationSeverity | undefined { + if (value === 'info' || value === 'warn' || value === 'error') { + return value; + } + return undefined; +} + +function parseMessages(value: unknown): ValidationMessage[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + return value.map((m) => { + if (typeof m !== 'object' || m === null) { + throw new Error('Invalid validation message'); + } + const msg = m as Record; + const severity = parseSeverity(msg.severity); + if (!severity || typeof msg.message !== 'string') { + throw new Error('Invalid validation message'); + } + return { + path: typeof msg.path === 'string' ? msg.path : undefined, + message: msg.message, + severity, + }; + }); +} + +function parseSuggestions( + value: unknown, +): import('./types.js').ValidationSuggestion[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + return value.map((s) => { + if (typeof s !== 'object' || s === null || typeof (s as { path?: unknown }).path !== 'string') { + throw new Error('Invalid validation suggestion'); + } + const sug = s as { path: string; value?: unknown }; + return { path: sug.path, value: sug.value }; + }); +} + +function parseMetrics(value: unknown): Record | undefined { + if (typeof value !== 'object' || value === null) { + return undefined; + } + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + if (typeof v === 'number') { + out[k] = v; + } + } + return Object.keys(out).length > 0 ? out : undefined; +} diff --git a/typescript/sdk/src/protocol/types.ts b/typescript/sdk/src/protocol/types.ts new file mode 100644 index 0000000..6c63f3c --- /dev/null +++ b/typescript/sdk/src/protocol/types.ts @@ -0,0 +1,169 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. +// Use of this source code is governed by a Apache-2.0 +// license that can be found in the LICENSE file. + +export type InterceptorType = 'validation' | 'mutation' | 'sink'; + +export type InterceptorPhase = 'request' | 'response'; + +/** SEP wire value for normal blocking / transforming behavior (default when omitted). */ +export type InterceptorMode = 'enforce' | 'audit'; + +export type ValidationSeverity = 'info' | 'warn' | 'error'; + +export type InterceptorChainStatus = + | 'success' + | 'validation_failed' + | 'mutation_failed' + | 'timeout'; + +export interface InterceptorHook { + events: string[]; + phase: InterceptorPhase; +} + +export interface InterceptorCompatibility { + minProtocol: string; + maxProtocol?: string; +} + +/** Per-phase mutation ordering hint (SEP-1763). Omitted sides default to 0. */ +export interface PriorityHintByPhase { + request?: number; + response?: number; +} + +/** Scalar applies to both phases; object selects per phase. */ +export type PriorityHint = number | PriorityHintByPhase; + +export interface Interceptor { + name: string; + version?: string; + description?: string; + type: InterceptorType; + hooks: InterceptorHook[]; + mode?: InterceptorMode; + failOpen?: boolean; + priorityHint?: PriorityHint; + compat?: InterceptorCompatibility; + configSchema?: unknown; + _meta?: Record; +} + +export interface ValidationMessage { + path?: string; + message: string; + severity: ValidationSeverity; +} + +export interface ValidationSuggestion { + path: string; + value?: unknown; +} + +export interface InterceptorPrincipal { + type: string; + id?: string; + claims?: Record; +} + +export interface InvokeInterceptorContext { + principal?: InterceptorPrincipal; + traceId?: string; + spanId?: string; + timestamp?: string; + sessionId?: string; +} + +export interface InterceptorResultBase { + type: InterceptorType; + interceptor?: string; + phase: InterceptorPhase; + durationMs?: number; + info?: Record; +} + +export interface ValidationInterceptorResult extends InterceptorResultBase { + type: 'validation'; + valid: boolean; + severity?: ValidationSeverity; + messages?: ValidationMessage[]; + suggestions?: ValidationSuggestion[]; +} + +export interface MutationInterceptorResult extends InterceptorResultBase { + type: 'mutation'; + modified: boolean; + payload?: unknown; +} + +export interface SinkInterceptorResult extends InterceptorResultBase { + type: 'sink'; + recorded: boolean; + metrics?: Record; +} + +export type InterceptorResult = + | ValidationInterceptorResult + | MutationInterceptorResult + | SinkInterceptorResult; + +export interface ListInterceptorsRequestParams { + cursor?: string; + event?: string; + _meta?: Record; +} + +export interface ListInterceptorsResult { + interceptors: Interceptor[]; + nextCursor?: string; +} + +export interface InvokeInterceptorRequestParams { + name: string; + event: string; + phase: InterceptorPhase; + payload: unknown; + config?: unknown; + timeoutMs?: number; + context?: InvokeInterceptorContext; + _meta?: Record; +} + +export interface ExecuteChainRequestParams { + event: string; + phase: InterceptorPhase; + payload: unknown; + /** Optional name filter (wire property: `interceptors`). */ + interceptors?: string[]; + config?: unknown; + timeoutMs?: number; + context?: InvokeInterceptorContext; +} + +export interface ChainValidationSummary { + errors: number; + warnings: number; + infos: number; +} + +export interface ChainAbortInfo { + interceptor: string; + reason: string; + type: string; +} + +export interface InterceptorChainResult { + status: InterceptorChainStatus; + event?: string; + phase: InterceptorPhase; + results: InterceptorResult[]; + finalPayload?: unknown; + validationSummary?: ChainValidationSummary; + totalDurationMs: number; + abortedAt?: ChainAbortInfo; +} + +export interface InterceptorsCapability { + supportedEvents: string[]; +} diff --git a/typescript/sdk/src/protocol/zod-schemas.ts b/typescript/sdk/src/protocol/zod-schemas.ts new file mode 100644 index 0000000..d9b3f20 --- /dev/null +++ b/typescript/sdk/src/protocol/zod-schemas.ts @@ -0,0 +1,124 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. +// Use of this source code is governed by a Apache-2.0 +// license that can be found in the LICENSE file. + +import * as z from 'zod/v4'; +import { RequestSchema, ResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import { InterceptorRequestMethods } from './constants.js'; + +const InterceptorPhaseSchema = z.enum(['request', 'response']); +const InterceptorTypeSchema = z.enum(['validation', 'mutation', 'sink']); +/** Accept C# SDK `"active"` on the wire and normalize to SEP `"enforce"`. */ +const InterceptorModeSchema = z + .enum(['enforce', 'audit', 'active']) + .transform((mode): 'enforce' | 'audit' => (mode === 'active' ? 'enforce' : mode)); +const ValidationSeveritySchema = z.enum(['info', 'warn', 'error']); + +const PriorityHintByPhaseSchema = z.object({ + request: z.number().optional(), + response: z.number().optional(), +}); + +const PriorityHintSchema = z.union([z.number(), PriorityHintByPhaseSchema]); + +export const InterceptorHookSchema = z.object({ + events: z.array(z.string()), + phase: InterceptorPhaseSchema, +}); + +export const InterceptorSchema = z.object({ + name: z.string(), + version: z.string().optional(), + description: z.string().optional(), + type: InterceptorTypeSchema, + hooks: z.array(InterceptorHookSchema), + mode: InterceptorModeSchema.optional(), + failOpen: z.boolean().optional(), + priorityHint: PriorityHintSchema.optional(), + compat: z + .object({ + minProtocol: z.string(), + maxProtocol: z.string().optional(), + }) + .optional(), + configSchema: z.unknown().optional(), + _meta: z.record(z.string(), z.unknown()).optional(), +}); + +export const ListInterceptorsRequestSchema = RequestSchema.extend({ + method: z.literal(InterceptorRequestMethods.InterceptorsList), + params: z + .object({ + cursor: z.string().optional(), + event: z.string().optional(), + _meta: z.record(z.string(), z.unknown()).optional(), + }) + .optional(), +}); + +export const ListInterceptorsResultSchema = ResultSchema.extend({ + interceptors: z.array(InterceptorSchema), + nextCursor: z.string().optional(), +}); + +export const InvokeInterceptorRequestSchema = RequestSchema.extend({ + method: z.literal(InterceptorRequestMethods.InterceptorInvoke), + params: z.object({ + name: z.string(), + event: z.string(), + phase: InterceptorPhaseSchema, + payload: z.unknown(), + config: z.unknown().optional(), + timeoutMs: z.number().optional(), + context: z.unknown().optional(), + _meta: z.record(z.string(), z.unknown()).optional(), + }), +}); + +const InterceptorResultBaseSchema = z.object({ + interceptor: z.string().optional(), + phase: InterceptorPhaseSchema, + durationMs: z.number().optional(), + info: z.record(z.string(), z.unknown()).optional(), +}); + +export const ValidationInterceptorResultSchema = InterceptorResultBaseSchema.extend({ + type: z.literal('validation'), + valid: z.boolean(), + severity: ValidationSeveritySchema.optional(), + messages: z + .array( + z.object({ + path: z.string().optional(), + message: z.string(), + severity: ValidationSeveritySchema, + }), + ) + .optional(), + suggestions: z + .array( + z.object({ + path: z.string(), + value: z.unknown().optional(), + }), + ) + .optional(), +}); + +export const MutationInterceptorResultSchema = InterceptorResultBaseSchema.extend({ + type: z.literal('mutation'), + modified: z.boolean(), + payload: z.unknown().optional(), +}); + +export const SinkInterceptorResultSchema = InterceptorResultBaseSchema.extend({ + type: z.literal('sink'), + recorded: z.boolean(), + metrics: z.record(z.string(), z.number()).optional(), +}); + +export const InterceptorResultSchema = z.discriminatedUnion('type', [ + ValidationInterceptorResultSchema, + MutationInterceptorResultSchema, + SinkInterceptorResultSchema, +]); diff --git a/typescript/sdk/src/server/capabilities.ts b/typescript/sdk/src/server/capabilities.ts new file mode 100644 index 0000000..e1f44da --- /dev/null +++ b/typescript/sdk/src/server/capabilities.ts @@ -0,0 +1,36 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import type { ServerCapabilities } from '@modelcontextprotocol/sdk/types.js'; +import { InterceptionEvents } from '../protocol/constants.js'; +import type { Interceptor, InterceptorsCapability } from '../protocol/types.js'; + +export function collectSupportedEvents(interceptors: Interceptor[]): string[] { + const events = new Set(); + for (const interceptor of interceptors) { + for (const hook of interceptor.hooks) { + for (const ev of hook.events) { + events.add(ev); + } + } + } + return [...events].sort(); +} + +export function buildInterceptorsCapability(interceptors: Interceptor[]): InterceptorsCapability { + const supported = collectSupportedEvents(interceptors); + return { + supportedEvents: supported.length > 0 ? supported : [InterceptionEvents.All], + }; +} + +/** Merge SEP `capabilities.interceptor` onto a v1 MCP server. */ +export function registerInterceptorCapabilities( + server: Server, + interceptors: Interceptor[], +): void { + const capability = buildInterceptorsCapability(interceptors); + server.registerCapabilities({ + interceptor: capability, + } as ServerCapabilities); +} diff --git a/typescript/sdk/src/server/interceptor-definition.ts b/typescript/sdk/src/server/interceptor-definition.ts new file mode 100644 index 0000000..13175f7 --- /dev/null +++ b/typescript/sdk/src/server/interceptor-definition.ts @@ -0,0 +1,47 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { InterceptionEvents } from '../protocol/constants.js'; +import type { + Interceptor, + InterceptorHook, + InterceptorMode, + InterceptorPhase, + InterceptorType, + PriorityHint, +} from '../protocol/types.js'; + +export type InterceptorPhaseOption = InterceptorPhase | 'both'; + +export interface InterceptorDefinitionOptions { + name: string; + type: InterceptorType; + description?: string; + events?: string[]; + phase?: InterceptorPhaseOption; + priorityHint?: PriorityHint; + mode?: InterceptorMode; + failOpen?: boolean; +} + +export function buildInterceptorDescriptor(options: InterceptorDefinitionOptions): Interceptor { + const events = options.events ?? [InterceptionEvents.All]; + const phase = options.phase ?? 'both'; + + const hooks: InterceptorHook[] = + phase === 'both' + ? [ + { events: [...events], phase: 'request' }, + { events: [...events], phase: 'response' }, + ] + : [{ events: [...events], phase }]; + + return { + name: options.name, + description: options.description, + type: options.type, + hooks, + mode: options.mode, + failOpen: options.failOpen, + priorityHint: options.priorityHint, + }; +} diff --git a/typescript/sdk/src/server/reflection.test.ts b/typescript/sdk/src/server/reflection.test.ts new file mode 100644 index 0000000..0df0e33 --- /dev/null +++ b/typescript/sdk/src/server/reflection.test.ts @@ -0,0 +1,91 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from '../protocol/constants.js'; +import { validationSuccess } from '../protocol/results.js'; +import { buildInterceptorDescriptor } from './interceptor-definition.js'; +import { defineInterceptor, invokeHandlerFunction } from './reflection.js'; + +describe('defineInterceptor / reflection', () => { + it('builds descriptor metadata from options', () => { + const d = buildInterceptorDescriptor({ + name: 'bool-validator', + type: 'validation', + events: [InterceptionEvents.ToolsCall], + phase: 'request', + }); + expect(d.name).toBe('bool-validator'); + expect(d.hooks).toHaveLength(1); + expect(d.hooks[0]?.events).toContain(InterceptionEvents.ToolsCall); + }); + + it('expands phase both to request and response hooks', () => { + const d = buildInterceptorDescriptor({ + name: 'sink', + type: 'sink', + phase: 'both', + }); + expect(d.hooks).toHaveLength(2); + expect(d.hooks.map((h) => h.phase)).toEqual(['request', 'response']); + }); + + it('wraps boolean return as validation result', async () => { + const reg = defineInterceptor( + { name: 'bool-validator', type: 'validation', events: [InterceptionEvents.ToolsCall] }, + (payload: unknown) => (payload as { valid?: boolean }).valid === true, + ); + + const ok = await reg.handler({ + name: 'bool-validator', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { valid: true }, + }); + expect(ok.type).toBe('validation'); + if (ok.type === 'validation') { + expect(ok.valid).toBe(true); + } + + const bad = await reg.handler({ + name: 'bool-validator', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { valid: false }, + }); + if (bad.type === 'validation') { + expect(bad.valid).toBe(false); + } + }); + + it('binds named parameters (payload, event, phase)', async () => { + const result = await invokeHandlerFunction( + (payload: unknown, event: string, phase: 'request' | 'response') => + validationSuccess(phase), + 'validation', + { + name: 'x', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }, + ); + expect(result.type).toBe('validation'); + }); + + it('supports async handlers', async () => { + const reg = defineInterceptor( + { name: 'async-val', type: 'validation' }, + async () => { + await Promise.resolve(); + return validationSuccess('request'); + }, + ); + const result = await reg.handler({ + name: 'async-val', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }); + expect(result.type).toBe('validation'); + }); +}); diff --git a/typescript/sdk/src/server/reflection.ts b/typescript/sdk/src/server/reflection.ts new file mode 100644 index 0000000..7de6943 --- /dev/null +++ b/typescript/sdk/src/server/reflection.ts @@ -0,0 +1,137 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { validationSuccess } from '../protocol/results.js'; +import type { + InterceptorResult, + InterceptorType, + InvokeInterceptorRequestParams, +} from '../protocol/types.js'; +import { + buildInterceptorDescriptor, + type InterceptorDefinitionOptions, +} from './interceptor-definition.js'; +import type { RegisteredInterceptor } from './register-interceptors.js'; + +export type InterceptorHandlerFn = (...args: unknown[]) => + | InterceptorResult + | Promise + | boolean + | Promise; + +/** + * Build a {@link RegisteredInterceptor} from a handler function and definition options + * (TypeScript equivalent of C# `[McpServerInterceptor]` + `ReflectionMcpServerInterceptor`). + */ +export function defineInterceptor( + options: InterceptorDefinitionOptions, + fn: InterceptorHandlerFn, +): RegisteredInterceptor { + return { + descriptor: buildInterceptorDescriptor(options), + handler: (params, signal) => invokeHandlerFunction(fn, options.type, params, signal), + }; +} + +export async function invokeHandlerFunction( + fn: InterceptorHandlerFn, + interceptorType: InterceptorType, + request: InvokeInterceptorRequestParams, + signal?: AbortSignal, +): Promise { + const args = bindHandlerArguments(fn, request, signal); + let result: unknown = fn(...args); + + if (result && typeof (result as Promise).then === 'function') { + result = await (result as Promise); + } + + return normalizeHandlerResult(result, request.phase, interceptorType); +} + +function bindHandlerArguments( + fn: InterceptorHandlerFn, + request: InvokeInterceptorRequestParams, + signal?: AbortSignal, +): unknown[] { + const params = new Map([ + ['payload', request.payload], + ['config', request.config], + ['event', request.event], + ['eventname', request.event], + ['phase', request.phase], + ['context', request.context], + ['cancellationtoken', signal], + ['ct', signal], + ]); + + const paramNames = getParameterNames(fn); + if (paramNames.length > 0) { + return paramNames.map((name) => { + const key = name.toLowerCase(); + if (params.has(key)) { + return params.get(key); + } + return undefined; + }); + } + + // Positional fallback (arity-based) + const arity = fn.length; + const positional: unknown[] = [request.payload]; + if (arity >= 2) { + positional.push(request.event); + } + if (arity >= 3) { + positional.push(request.phase); + } + if (arity >= 4) { + positional.push(request.context); + } + if (arity >= 5) { + positional.push(signal); + } + return positional.slice(0, arity); +} + +function getParameterNames(fn: InterceptorHandlerFn): string[] { + const src = fn.toString(); + const match = src.match(/^[^(]*\(([^)]*)\)/); + if (!match?.[1]?.trim()) { + return []; + } + return match[1] + .split(',') + .map((p) => p.trim().split(/\s/)[0]?.replace(/[?[\]]/g, '') ?? '') + .filter((n) => n.length > 0 && n !== ''); +} + +function normalizeHandlerResult( + result: unknown, + phase: InvokeInterceptorRequestParams['phase'], + interceptorType: InterceptorType, +): InterceptorResult { + if (typeof result === 'boolean') { + if (interceptorType !== 'validation') { + throw new Error(`Boolean return is only supported for validation interceptors`); + } + return result + ? validationSuccess(phase) + : { + type: 'validation', + phase, + valid: false, + severity: 'error', + messages: [{ message: 'Validation failed', severity: 'error' }], + }; + } + + if (typeof result !== 'object' || result === null || !('type' in result)) { + throw new Error( + `Interceptor handler must return InterceptorResult or boolean, got ${typeof result}`, + ); + } + + const typed = result as InterceptorResult; + typed.phase = phase; + return typed; +} diff --git a/typescript/sdk/src/server/register-interceptors.test.ts b/typescript/sdk/src/server/register-interceptors.test.ts new file mode 100644 index 0000000..ac7ad41 --- /dev/null +++ b/typescript/sdk/src/server/register-interceptors.test.ts @@ -0,0 +1,141 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import { describe, it, expect } from 'vitest'; +import { InterceptionEvents } from '../protocol/constants.js'; +import { validationSuccess } from '../protocol/results.js'; +import { listInterceptors, invokeInterceptor } from '../client/client-extensions.js'; +import { collectSupportedEvents } from './capabilities.js'; +import { connectInterceptorHost } from '../__tests__/fixtures/hosts.js'; +import type { RegisteredInterceptor } from './register-interceptors.js'; + +const toolsValidator: RegisteredInterceptor = { + descriptor: { + name: 'tools-only', + type: 'validation', + hooks: [{ events: [InterceptionEvents.ToolsCall], phase: 'request' }], + }, + handler: () => validationSuccess('request'), +}; + +const promptsValidator: RegisteredInterceptor = { + descriptor: { + name: 'prompts-only', + type: 'validation', + hooks: [{ events: [InterceptionEvents.PromptsGet], phase: 'request' }], + }, + handler: () => validationSuccess('request'), +}; + +describe('registerInterceptorsOnServer', () => { + it('advertises capabilities.interceptor from hook events', () => { + const events = collectSupportedEvents([ + toolsValidator.descriptor, + promptsValidator.descriptor, + ]); + expect(events).toContain(InterceptionEvents.ToolsCall); + expect(events).toContain(InterceptionEvents.PromptsGet); + }); + + it('lists and filters by event', async () => { + const { client, server, close } = await connectInterceptorHost([ + toolsValidator, + promptsValidator, + ]); + + type Caps = { interceptor?: { supportedEvents: string[] } }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const caps = server.getCapabilities() as Caps; + expect(caps.interceptor?.supportedEvents).toContain(InterceptionEvents.ToolsCall); + + const all = await listInterceptors(client); + expect(all.interceptors).toHaveLength(2); + + const toolsOnly = await listInterceptors(client, { event: InterceptionEvents.ToolsCall }); + expect(toolsOnly.interceptors).toHaveLength(1); + expect(toolsOnly.interceptors[0]?.name).toBe('tools-only'); + + await close(); + }); + + it('invokes registered handler', async () => { + const { client, close } = await connectInterceptorHost([ + { + descriptor: { + name: 'echo-val', + type: 'validation', + hooks: [{ events: [InterceptionEvents.All], phase: 'request' }], + }, + handler: (params) => ({ + type: 'validation', + phase: params.phase, + valid: true, + }), + }, + ]); + + const result = await invokeInterceptor(client, { + name: 'echo-val', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: { test: 1 }, + }); + + expect(result.type).toBe('validation'); + expect(result.interceptor).toBe('echo-val'); + + await close(); + }); + + it('invoke throws when interceptor name is unknown', async () => { + const { client, close } = await connectInterceptorHost([toolsValidator]); + + await expect( + invokeInterceptor(client, { + name: 'missing', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + }), + ).rejects.toThrow(/not found/i); + + await close(); + }); + + it('invoke times out slow handlers', async () => { + const slow: RegisteredInterceptor = { + descriptor: { + name: 'slow', + type: 'validation', + hooks: [{ events: [InterceptionEvents.All], phase: 'request' }], + }, + handler: async (_params, signal) => { + await new Promise((resolve, reject) => { + const timer = setTimeout(resolve, 500); + signal?.addEventListener( + 'abort', + () => { + clearTimeout(timer); + reject(new Error('aborted')); + }, + { once: true }, + ); + }); + return validationSuccess('request'); + }, + }; + + const { client, close } = await connectInterceptorHost([slow]); + + await expect( + invokeInterceptor(client, { + name: 'slow', + event: InterceptionEvents.ToolsCall, + phase: 'request', + payload: {}, + timeoutMs: 40, + }), + ).rejects.toThrow(/timed out/i); + + await close(); + }); +}); diff --git a/typescript/sdk/src/server/register-interceptors.ts b/typescript/sdk/src/server/register-interceptors.ts new file mode 100644 index 0000000..741b11e --- /dev/null +++ b/typescript/sdk/src/server/register-interceptors.ts @@ -0,0 +1,99 @@ +// Copyright 2025 The MCP Interceptors Authors. All rights reserved. + +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { matchesEvent } from '../client/chain-orchestrator.js'; +import type { + Interceptor, + InterceptorResult, + InvokeInterceptorRequestParams, + ListInterceptorsRequestParams, +} from '../protocol/types.js'; +import { + InterceptorResultSchema, + InvokeInterceptorRequestSchema, + ListInterceptorsRequestSchema, + ListInterceptorsResultSchema, +} from '../protocol/zod-schemas.js'; +import { registerInterceptorCapabilities } from './capabilities.js'; + +export type InterceptorHandler = ( + params: InvokeInterceptorRequestParams, + signal?: AbortSignal, +) => InterceptorResult | Promise; + +export interface RegisteredInterceptor { + descriptor: Interceptor; + handler: InterceptorHandler; +} + +export interface RegisterInterceptorsOptions { + /** When true (default), merge `capabilities.interceptor` from registered hooks. */ + registerCapabilities?: boolean; +} + +export function registerInterceptorsOnServer( + server: Server, + interceptors: RegisteredInterceptor[], + options?: RegisterInterceptorsOptions, +): void { + const registerCaps = options?.registerCapabilities !== false; + const descriptors = interceptors.map((e) => e.descriptor); + const byName = new Map(interceptors.map((e) => [e.descriptor.name, e])); + + if (registerCaps) { + registerInterceptorCapabilities(server, descriptors); + } + + server.setRequestHandler(ListInterceptorsRequestSchema, (request) => { + const params = request.params as ListInterceptorsRequestParams | undefined; + const eventFilter = params?.event; + const listed: Interceptor[] = []; + + for (const entry of interceptors) { + if (eventFilter) { + const matchesAnyHook = entry.descriptor.hooks.some((hook) => + matchesEvent(hook.events, eventFilter), + ); + if (!matchesAnyHook) { + continue; + } + } + listed.push(entry.descriptor); + } + + return { interceptors: listed }; + }); + + server.setRequestHandler(InvokeInterceptorRequestSchema, async (request) => { + const params = request.params as InvokeInterceptorRequestParams; + const entry = byName.get(params.name); + if (!entry) { + throw new Error(`Interceptor '${params.name}' not found`); + } + + const signal = + params.timeoutMs != null ? AbortSignal.timeout(params.timeoutMs) : undefined; + + try { + const result = await entry.handler(params, signal); + result.interceptor = entry.descriptor.name; + result.phase = params.phase; + return result as unknown as Record; + } catch (err) { + if (signal?.aborted) { + throw new Error( + `Interceptor '${params.name}' timed out after ${params.timeoutMs}ms`, + ); + } + throw err; + } + }); +} + +/** @internal For tests validating handler registration schemas. */ +export const interceptorWireSchemas = { + ListInterceptorsRequestSchema, + ListInterceptorsResultSchema, + InvokeInterceptorRequestSchema, + InterceptorResultSchema, +}; diff --git a/typescript/sdk/tsconfig.build.json b/typescript/sdk/tsconfig.build.json new file mode 100644 index 0000000..deebc05 --- /dev/null +++ b/typescript/sdk/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "src/**/__tests__/**", "src/**/*.test.ts"] +} diff --git a/typescript/sdk/tsconfig.eslint.json b/typescript/sdk/tsconfig.eslint.json new file mode 100644 index 0000000..631f88a --- /dev/null +++ b/typescript/sdk/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/typescript/sdk/vitest.config.js b/typescript/sdk/vitest.config.js index bfa5448..c46721d 100644 --- a/typescript/sdk/vitest.config.js +++ b/typescript/sdk/vitest.config.js @@ -4,7 +4,7 @@ export default defineConfig({ test: { globals: true, environment: 'node', - include: ['src/**/*.test.ts', 'test/**/*.test.ts'], + include: ['src/**/*.test.ts', 'src/**/__tests__/**/*.test.ts', 'test/**/*.test.ts'], exclude: ['**/node_modules/**', '**/dist/**'] } }); \ No newline at end of file