diff --git a/.agents/skills/check-changed-folders/SKILL.md b/.agents/skills/check-changed-folders/SKILL.md index 435bbb7..6c1ef77 100644 --- a/.agents/skills/check-changed-folders/SKILL.md +++ b/.agents/skills/check-changed-folders/SKILL.md @@ -1,11 +1,11 @@ --- name: check-changed-folders -description: Run typecheck, tests, and builds for Rudder's core and plugin workspaces on the current branch versus main, verify the centralized agent-instruction layout, and verify agent attribution. Use when asked to run "/check", to validate a branch before commit/PR, or whenever a user asks to run checks before publishing. +description: Run typecheck, tests, and builds for Rudder's core and plugin workspaces on the current branch versus main, verify Claude/Codex provider parity, verify the centralized agent-instruction layout, and verify agent attribution. Use when asked to run "/check", to validate a branch before commit/PR, whenever a user asks to run checks before publishing, or whenever changes affect provider-facing plugin surfaces such as manifests, hooks, skills, MCP servers, apps, or UI. --- # Check Changed Folders -Identify what changed on the branch, verify the centralized agent-instruction layout and agent attribution, run the package checks, and report pass/fail status with actionable failure output. +Identify what changed on the branch, verify provider parity, the centralized agent-instruction layout, and agent attribution, run the package checks, and report pass/fail status with actionable failure output. The root npm scripts validate the core package and the Rudder plugin workspace. Checks are therefore repo-wide rather than per-package. @@ -20,7 +20,7 @@ git diff --name-only git diff --name-only --cached ``` - If anything under `src/`, `bin/`, `test/`, or a build/config file (`package.json`, `tsconfig*.json`, `.github/`) changed, the package checks below apply. + If anything under `src/`, `bin/`, `test/`, `ui/`, `.claude-plugin/`, `.codex-plugin/`, `hooks/`, `skills/`, or a build/config file (`package.json`, `tsconfig*.json`, `.mcp.json`, `.github/`) changed, the package checks below apply. 2. Verify the centralized agent-instruction layout before running checks: @@ -33,16 +33,27 @@ git diff --name-only --cached - `.codex/skills` -> `../.agents/skills` - If any link is missing or resolves outside `.agents/`, mark the check as failed and report the broken path. -3. Verify agent attribution. +3. Verify provider parity. + +- Treat Claude and Codex as supported providers for the same Rudder plugin package. +- When a change adds, removes, or modifies a provider-facing component—manifest metadata, skills, hooks, MCP servers, apps, UI, or packaged artifacts—inspect both provider manifests and runtime paths. +- Require equivalent discovery, launch behavior, packaged resources, and user-facing capability across providers. + Provider-specific schema or environment-variable differences are allowed, but they must remain explicit implementation details rather than functional gaps. +- Add or update a regression assertion in `test/plugin-package.test.ts` or the relevant runtime test that exercises both provider paths. + If provider-facing behavior changes without parity coverage, fail this check. +- Permit a provider-specific exception only when the user explicitly requests it or the provider cannot support the capability. + Report the reason, the affected provider, and the test that protects the supported behavior. + +4. Verify agent attribution. If a coding agent wrote code included in the branch, inspect `git log origin/main..HEAD` and require every such agent to appear as a commit author or in a `Co-authored-by:` trailer. Missing agent attribution on committed work fails the check. If the agent-written work is still uncommitted, report attribution as pending and name the trailer that must be added when committing. Human-only changes are not subject to this check. -4. Install dependencies if `node_modules/` does not exist: run `npm install`. +5. Install dependencies if `node_modules/` does not exist: run `npm install`. -5. Run the package checks (the same set `prepublishOnly` runs, so green means publishable): +6. Run the package checks (the same set `prepublishOnly` runs, so green means publishable): ```bash npm run typecheck @@ -50,22 +61,23 @@ npm test npm run build ``` -6. Surface and address open PR comments. +7. Surface and address open PR comments. If the current branch has an open GitHub PR, always invoke the `address-pr-comments` skill before finishing. That skill fetches open review comments (Greptile, human reviewers) for the PR, dedupes them, and fixes/declines/defers each one. Only if `gh` is unavailable or there is no PR for the current branch, treat this step as `skipped`. If any comment is acted on, re-run the checks before reporting. -7. Report concise results: +8. Report concise results: - State whether typecheck, tests, and build passed, failed, or were skipped. -- Include dedicated results for agent-instruction layout, agent attribution, and PR comments (`passed`, `failed`, `pending`, or `skipped`) and why. +- Include dedicated results for provider parity, agent-instruction layout, agent attribution, and PR comments (`passed`, `failed`, `pending`, or `skipped`) and why. - For failures, include the key error output and which command failed. -- Distinguish real failures (broken agent links, missing agent attribution, type errors, test failures, unaddressed P0/P1 PR comments) from environment issues (missing CLI tools, no PR). +- Distinguish real failures (provider drift, missing parity coverage, broken agent links, missing agent attribution, type errors, test failures, unaddressed P0/P1 PR comments) from environment issues (missing CLI tools, no PR). ## Notes - Default comparison branch is `origin/main` (not local `main`, which may be stale). +- Provider parity means equivalent behavior and coverage, not necessarily byte-identical provider configuration. - The `.agents/` layout is a required gate, not an optional reminder. - Agent attribution is required only when an agent contributed code. - If nothing relevant changed, state that no checks were required. diff --git a/.agents/skills/check-changed-folders/agents/openai.yaml b/.agents/skills/check-changed-folders/agents/openai.yaml index 1e51492..e9c08fc 100644 --- a/.agents/skills/check-changed-folders/agents/openai.yaml +++ b/.agents/skills/check-changed-folders/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Check" - short_description: "Run checks and verify agent attribution and the shared workflow layout." - default_prompt: "Use $check-changed-folders to verify agent attribution and the .agents-based workflow layout, then run typecheck, tests, and build." + short_description: "Validate provider parity, layout, attribution, and builds." + default_prompt: "Use $check-changed-folders to verify Claude/Codex provider parity, attribution, and the shared workflow layout, then run the repository checks." diff --git a/.claude-mcp.json b/.claude-mcp.json new file mode 100644 index 0000000..9cff529 --- /dev/null +++ b/.claude-mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "rudder": { + "command": "node", + "args": [ + "--input-type=module", + "-e", + "import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const root = process.env.PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT || process.cwd(); await import(pathToFileURL(resolve(root, 'dist/rudder-mcp-server.mjs')).href)" + ], + "cwd": "." + } + } +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 7712ae9..60ff60e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -17,5 +17,6 @@ "coverage" ], "skills": "./skills/", + "mcpServers": "./.claude-mcp.json", "hooks": "./hooks/hooks.json" } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 565067b..83bdc77 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -18,6 +18,7 @@ "coverage" ], "skills": "./skills/", + "mcpServers": "./.mcp.json", "interface": { "displayName": "Rudder", "shortDescription": "Tests from intent and changes", @@ -25,6 +26,7 @@ "developerName": "RudderCode", "category": "Productivity", "capabilities": [ + "Interactive", "Write" ], "websiteURL": "https://github.com/RudderCode/Rudder", diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..09a58ca --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "rudder": { + "command": "node", + "args": [ + "--input-type=module", + "-e", + "import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const root = process.env.PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT || process.cwd(); await import(pathToFileURL(resolve(root, 'dist/rudder-mcp-server.mjs')).href)" + ], + "cwd": "." + } +} diff --git a/almanac/architecture/automation/contributor-automation.md b/almanac/architecture/automation/contributor-automation.md index 5c736dd..df049d6 100644 --- a/almanac/architecture/automation/contributor-automation.md +++ b/almanac/architecture/automation/contributor-automation.md @@ -1,6 +1,6 @@ --- title: "Contributor Automation" -summary: "Contributor automation connects centralized agent skills, local check flows, PR-comment remediation, CI validation, and Danger-based agent guards into one guarded branch workflow." +summary: "Contributor automation connects centralized agent skills, provider-parity checks, local check flows, PR-comment remediation, CI validation, and Danger-based agent guards into one guarded branch workflow." topics: [architecture, automation, contributor-workflow, validation] sources: - id: agents-readme @@ -29,14 +29,16 @@ sources: path: dangerfile.ts --- -Rudder's contributor automation is a set of local and CI gates for a repository that currently has one root plugin package and centralized agent workflows. `.agents/skills/` is the only reusable-workflow source, with `.claude/skills` and `.codex/skills` as compatibility symlinks [@agents-readme]. The `check-changed-folders` skill validates branches, verifies layout and attribution, runs local package checks, and delegates PR-comment remediation when a PR exists [@check-skill]. The `prepare-package-release` skill synchronizes package and plugin versions, ingests the complete range since the previous release, Gardens the whole CodeAlmanac wiki, and validates the prepared release [@release-skill]. GitHub Actions repeats package validation on branch pushes, while the Danger workflow enforces protected paths and inline agent guards for agent-authored pull requests [@test-workflow] [@danger-workflow] [@dangerfile]. +Rudder's contributor automation is a set of local and CI gates for a repository that currently has one root plugin package and centralized agent workflows. `.agents/skills/` is the only reusable-workflow source, with `.claude/skills` and `.codex/skills` as compatibility symlinks [@agents-readme]. The `check-changed-folders` skill validates branches, verifies provider parity, layout, and attribution, runs local package checks, and delegates PR-comment remediation when a PR exists [@check-skill]. The `prepare-package-release` skill synchronizes package and plugin versions, ingests the complete range since the previous release, Gardens the whole CodeAlmanac wiki, and validates the prepared release [@release-skill]. GitHub Actions repeats package validation on branch pushes, while the Danger workflow enforces protected paths and inline agent guards for agent-authored pull requests [@test-workflow] [@danger-workflow] [@dangerfile]. ## Local Check Surface -The check surface is centralized in `.agents/skills/check-changed-folders/SKILL.md` [@check-skill]. It starts by fetching `origin/main`, collecting changed files from the merge-base diff plus unstaged and staged local changes, and treating Rudder as repo-wide rather than per-package [@check-skill]. When files under `src/`, `bin/`, `test/`, package or TypeScript configuration, or `.github/` change, the package checks apply [@check-skill]. +The check surface is centralized in `.agents/skills/check-changed-folders/SKILL.md` [@check-skill]. It starts by fetching `origin/main`, collecting changed files from the merge-base diff plus unstaged and staged local changes, and treating Rudder as repo-wide rather than per-package [@check-skill]. When files under `src/`, `bin/`, `test/`, `ui/`, `.claude-plugin/`, `.codex-plugin/`, `hooks/`, `skills/`, package or TypeScript configuration, `.mcp.json`, or `.github/` change, the package checks apply [@check-skill]. The centralized layout itself is a hard gate. The check skill requires `AGENTS.md` as canonical guidance, `.agents/skills/` as the only reusable workflow source, `skills//agents/openai.yaml` metadata for shared skills, `.claude/skills` and `.codex/skills` symlinks to `.agents/skills`, and no `.claude/commands` aliases [@agents-readme] [@check-skill]. The [Run Checks](../../guides/contributor/run-checks) guide turns this architecture into the step-by-step contributor procedure. +Provider parity is a hard local gate for provider-facing changes. The check skill treats Claude and Codex as supported providers for the same package, requires equivalent discovery, launch behavior, packaged resources, and user-facing capability, and requires regression coverage in `test/plugin-package.test.ts` or the relevant runtime test for both provider paths [@check-skill]. Provider-specific schema or environment-variable differences are allowed only when they do not become functional gaps; exceptions need an explicit user request or an unsupported provider capability, plus a reported reason and protective test for the supported path [@check-skill]. + Before package checks, the local flow also checks that each coding agent represented in committed work is listed as a commit author or `Co-authored-by` trailer. Agent-written uncommitted work is reported as pending attribution until it is committed; human-only changes are outside this gate [@check-skill]. ## Package Checks diff --git a/almanac/architecture/tooling/package-baseline.md b/almanac/architecture/tooling/package-baseline.md index 0687f94..405ffb2 100644 --- a/almanac/architecture/tooling/package-baseline.md +++ b/almanac/architecture/tooling/package-baseline.md @@ -1,7 +1,7 @@ --- title: "Package Baseline" -summary: "The package baseline defines Rudder's scoped ESM plugin package, validation scripts, bundled hook output, dependency set, and publishable files." -topics: [architecture, tooling, package, typescript, plugin] +summary: "The package baseline defines Rudder's scoped ESM plugin package, validation scripts, bundled hook and MCP App output, dependency set, and publishable files." +topics: [architecture, tooling, package, typescript, plugin, mcp-app] sources: - id: package-json type: file @@ -12,6 +12,18 @@ sources: - id: hook-bin type: file path: bin/rudder-prompt-hook.ts + - id: mcp-config + type: file + path: .mcp.json + - id: mcp-bin + type: file + path: bin/rudder-mcp-server.ts + - id: mcp-runner + type: file + path: src/mcp/run-server.ts + - id: app-builder + type: file + path: scripts/build-mcp-app.mjs - id: plugin-tests type: file path: test/plugin-package.test.ts @@ -20,28 +32,28 @@ sources: path: .gitignore --- -Rudder's package baseline is the repo's build and distribution frame for the plugin. The package is published as `@ruddercode/rudder-plugin`, uses ESM, requires Node `>=24.0.0`, bundles the prompt-capture and telemetry entrypoint to `dist/rudder-prompt-hook.mjs`, copies generated Drizzle migrations into `dist/drizzle`, and includes plugin manifests, assets, docs, hooks, skills, `dist`, and `LICENSE` in the npm file allowlist [@package-json] [@hook-bin]. That baseline connects runtime, plugin, and tooling work to [Contributor Automation](../automation/contributor-automation), because the package scripts define the command set those pages reuse [@package-json]. +Rudder's package baseline is the repo's build and distribution frame for the plugin. The package is published as `@ruddercode/rudder-plugin`, uses ESM, requires Node `>=24.0.0`, bundles the prompt-capture and telemetry entrypoint to `dist/rudder-prompt-hook.mjs`, bundles the MCP server to `dist/rudder-mcp-server.mjs`, generates the single-file app resource at `dist/rudder-app.html`, copies generated Drizzle migrations into `dist/drizzle`, and includes `.mcp.json`, plugin manifests, assets, docs, hooks, skills, `dist`, and `LICENSE` in the npm file allowlist [@package-json] [@hook-bin]. That baseline connects runtime, plugin, and tooling work to [Contributor Automation](../automation/contributor-automation), because the package scripts define the command set those pages reuse [@package-json]. ## Package Contract -`package.json` is the public package contract. It names the package, carries the current release version, declares the Apache-2.0 license and GitHub repository metadata, sets the supported Node engine, and defines package scripts plus the package file allowlist [@package-json]. The manifest intentionally carries plugin artifacts rather than a `main`, `types`, or package-root `exports` entry; plugin hosts load the installed hook through `hooks/hooks.json`, and tests enforce that the manifest does not define runtime dependencies or npm workspaces [@package-json] [@plugin-tests]. +`package.json` is the public package contract. It names the package, carries the current release version, declares the Apache-2.0 license and GitHub repository metadata, sets the supported Node engine, and defines package scripts plus the package file allowlist [@package-json]. The manifest intentionally carries plugin artifacts rather than a `main`, `types`, or package-root `exports` entry; plugin hosts load the installed hook through `hooks/hooks.json` and load the MCP server through `.mcp.json`, while tests enforce that the manifest does not define runtime dependencies or npm workspaces [@package-json] [@mcp-config] [@plugin-tests]. -The package file allowlist keeps distribution narrow but plugin-complete. `.claude-plugin`, `.codex-plugin`, `assets`, `docs`, `hooks`, `skills`, `dist`, and `LICENSE` ship with the package [@package-json]. Build output and transient development files are excluded from the working tree by `.gitignore`, which ignores `node_modules/`, `dist/`, TypeScript build info, logs, `.env` files except `.env.example`, coverage, and common editor files [@gitignore]. +The package file allowlist keeps distribution narrow but plugin-complete. `.mcp.json`, `.claude-plugin`, `.codex-plugin`, `assets`, `docs`, `hooks`, `skills`, `dist`, and `LICENSE` ship with the package [@package-json]. Build output and transient development files are excluded from the working tree by `.gitignore`, which ignores `node_modules/`, `dist/`, TypeScript build info, logs, `.env` files except `.env.example`, coverage, and common editor files [@gitignore]. ## TypeScript Shape -The TypeScript configuration targets `ES2023`, uses `NodeNext` module and module-resolution behavior, enables `strict`, sets Node types, and includes `bin/**/*.ts`, `dangerfile.ts`, and `src/**/*.ts` [@tsconfig]. The configuration uses `noEmit: true`; package build output comes from esbuild, not from a separate TypeScript emit overlay [@tsconfig] [@package-json]. +The TypeScript configuration targets `ES2023`, uses `NodeNext` module and module-resolution behavior, enables `strict`, sets Node types, includes the `DOM` library for the MCP App UI source, and includes `bin/**/*.ts`, `dangerfile.ts`, `src/**/*.ts`, and `ui/**/*.ts` [@tsconfig]. The configuration uses `noEmit: true`; package build output comes from esbuild and the app resource builder, not from a separate TypeScript emit overlay [@tsconfig] [@package-json] [@app-builder]. -The bundled hook imports repository runtime modules from `bin/rudder-prompt-hook.ts`, selects prompt-capture or internal Rudder-event mode from its arguments, and is bundled for Node ESM output at `dist/rudder-prompt-hook.mjs` [@hook-bin] [@package-json]. The companion [TypeScript build reference](../../reference/tooling/typescript-build) records the exact compiler and bundle contract as lookup material. +The bundled hook imports repository runtime modules from `bin/rudder-prompt-hook.ts` and selects prompt-capture or internal Rudder-event mode from its arguments, while the MCP server entrypoint delegates to `runRudderMcpServer()` from `bin/rudder-mcp-server.ts` [@hook-bin] [@mcp-bin]. The `build` script bundles both entrypoints for Node ESM output and then runs the MCP App builder before copying migrations [@package-json] [@app-builder]. The companion [TypeScript build reference](../../reference/tooling/typescript-build) records the exact compiler and bundle contract as lookup material. ## Scripts And Validation -The baseline includes scripts for migration generation, markdown formatting, Danger, agent-layout validation, typechecking, hook bundling, tests, changed-line coverage, packing, and publishing [@package-json]. `typecheck` runs `tsc --noEmit`, `test` runs Node's built-in test runner, `test:coverage` builds and runs that suite under c8 before requiring 90% coverage on changed and untracked lines with `diff-cover`, `pretest` rebuilds the hook bundle, `build` clears `dist`, runs esbuild, and copies `drizzle/`, and `prepack` rebuilds before npm packing [@package-json]. The package-level script contract is described in [Package Scripts](../../reference/tooling/package-scripts). +The baseline includes scripts for migration generation, markdown formatting, Danger, agent-layout validation, typechecking, package bundling, tests, changed-line coverage, packing, and publishing [@package-json]. `typecheck` runs `tsc --noEmit`, `test` runs Node's built-in test runner, `test:coverage` builds and runs that suite under c8 before requiring 90% coverage on changed and untracked lines with `diff-cover`, `pretest` rebuilds the package artifacts, `build` clears `dist`, bundles the prompt hook and MCP server, builds the app HTML resource, and copies `drizzle/`, and `prepack` rebuilds before npm packing [@package-json]. The package-level script contract is described in [Package Scripts](../../reference/tooling/package-scripts). -This structure makes the package baseline small but not inert. Future runtime or hook code must fit the existing NodeNext TypeScript model, keep the esbuild output under `dist`, keep generated migrations available to installed hook code, and keep the validation scripts green before package publication [@tsconfig] [@package-json]. +This structure makes the package baseline small but not inert. Future runtime, hook, or MCP App code must fit the existing NodeNext TypeScript model, keep generated package artifacts under `dist`, keep generated migrations available to installed hook code, keep the MCP server and app resource available to the `.mcp.json` launcher and server runner, and keep the validation scripts green before package publication [@tsconfig] [@package-json] [@mcp-config] [@mcp-runner]. ## Dependency Boundary -The root package currently lists Drizzle ORM, PostHog's Node client, c8, diff-cover, esbuild, Danger, rumdl, TypeScript, Drizzle Kit, and Node type definitions as development dependencies, with Drizzle ORM and Drizzle Kit pinned to `1.0.0-rc.4` [@package-json]. c8 produces LCOV for all configured TypeScript and skill-script sources, while diff-cover compares that report with Git changes [@package-json]. Because the hook bundle is self-contained and `package.json` has no runtime `dependencies` field, changing dependency scope should be treated as a package-contract change and checked against [Rudder Plugin Package](plugin-package) [@package-json] [@plugin-tests]. +The root package currently lists the MCP SDK, MCP Apps extension package, Zod, Drizzle ORM, PostHog's Node client, c8, diff-cover, esbuild, Danger, rumdl, TypeScript, Drizzle Kit, and Node type definitions as development dependencies, with Drizzle ORM and Drizzle Kit pinned to `1.0.0-rc.4` [@package-json]. c8 produces LCOV for all configured TypeScript, UI, and skill-script sources, while diff-cover compares that report with Git changes [@package-json]. Because the hook and MCP server bundles are self-contained and `package.json` has no runtime `dependencies` field, changing dependency scope should be treated as a package-contract change and checked against [Rudder Plugin Package](plugin-package) [@package-json] [@plugin-tests]. Package changes should be read together with [contributor automation](../automation/contributor-automation). That page explains how local check surfaces and GitHub Actions reuse the `typecheck`, `test`, and `build` scripts documented here. diff --git a/almanac/architecture/tooling/plugin-package.md b/almanac/architecture/tooling/plugin-package.md index 6eb233a..9ffbc52 100644 --- a/almanac/architecture/tooling/plugin-package.md +++ b/almanac/architecture/tooling/plugin-package.md @@ -1,7 +1,7 @@ --- title: "Rudder Plugin Package" -summary: "The root npm package distributes Rudder as one Claude Code and Codex plugin with manifests, hooks, skill files, docs, assets, and a bundled prompt-capture and telemetry runtime." -topics: [architecture, tooling, package, plugin, prompt-capture, release] +summary: "The root npm package distributes Rudder as one Claude Code and Codex plugin with manifests, hooks, skill files, docs, assets, a prompt-capture and telemetry runtime, and an MCP App runtime." +topics: [architecture, tooling, package, plugin, prompt-capture, mcp-app, release] sources: - id: package-json type: file @@ -21,6 +21,24 @@ sources: - id: hook-bin type: file path: bin/rudder-prompt-hook.ts + - id: mcp-config + type: file + path: .mcp.json + - id: mcp-bin + type: file + path: bin/rudder-mcp-server.ts + - id: mcp-runner + type: file + path: src/mcp/run-server.ts + - id: mcp-server + type: file + path: src/mcp/server.ts + - id: app-builder + type: file + path: scripts/build-mcp-app.mjs + - id: app-source + type: file + path: ui/rudder-app.ts - id: skill type: file path: skills/rudder/SKILL.md @@ -33,6 +51,12 @@ sources: - id: plugin-tests type: file path: test/plugin-package.test.ts + - id: mcp-tests + type: file + path: test/mcp-server.test.ts + - id: ui-tests + type: file + path: test/ui.test.ts - id: install-doc type: file path: docs/install.md @@ -43,11 +67,11 @@ sources: # Rudder Plugin Package -The repository root is the publishable Rudder plugin package. `package.json` names the package `@ruddercode/rudder-plugin`, requires Node `>=24.0.0`, and includes plugin-specific artifacts such as `.claude-plugin`, `.codex-plugin`, `assets`, `docs`, `hooks`, `skills`, and `dist` in the npm file allowlist [@package-json]. The package carries both Claude Code and Codex plugin manifests, a public marketplace catalog that points at the npm package, the Rudder skill and helper scripts, and one bundled runtime used for prompt capture and bounded product telemetry [@claude-manifest] [@codex-manifest] [@marketplace] [@hooks] [@skill] [@skill-telemetry] [@rudder-telemetry]. +The repository root is the publishable Rudder plugin package. `package.json` names the package `@ruddercode/rudder-plugin`, requires Node `>=24.0.0`, and includes plugin-specific artifacts such as `.mcp.json`, `.claude-plugin`, `.codex-plugin`, `assets`, `docs`, `hooks`, `skills`, and `dist` in the npm file allowlist [@package-json]. The package carries both Claude Code and Codex plugin manifests, a public marketplace catalog that points at the npm package, the Rudder skill and helper scripts, one bundled runtime used for prompt capture and bounded product telemetry, and the MCP App runtime that opens Rudder's interactive UI [@claude-manifest] [@codex-manifest] [@marketplace] [@hooks] [@skill] [@skill-telemetry] [@rudder-telemetry] [@mcp-config] [@mcp-server]. ## Distribution Shape -The Claude manifest and Codex manifest share the public plugin name `rudder`, version, description, license, repository, keywords, and `./skills/` path [@claude-manifest] [@codex-manifest]. The Claude manifest also points at `./hooks/hooks.json`, while the Codex manifest carries interface metadata such as display name, short description, category, default prompt, icon, logo, privacy URL, and terms URL [@claude-manifest] [@codex-manifest]. +The Claude manifest and Codex manifest share the public plugin name `rudder`, version, description, license, repository, keywords, `./skills/` path, and `./.mcp.json` MCP server configuration path [@claude-manifest] [@codex-manifest]. The Claude manifest also points at `./hooks/hooks.json`, while the Codex manifest carries interface metadata such as display name, short description, category, default prompt, icon, logo, privacy URL, terms URL, and the `Interactive` capability [@claude-manifest] [@codex-manifest]. Package tests enforce that the two manifests keep matching `skills` and `mcpServers` fields, that Codex declares `Interactive`, and that `.mcp.json` is included in the npm file allowlist [@plugin-tests]. The marketplace catalog under `.claude-plugin/marketplace.json` lists one plugin named `rudder` and resolves it from npm package `@ruddercode/rudder-plugin` on the public npm registry [@marketplace]. Tests enforce that the package version is synchronized across `package.json`, `package-lock.json`, both plugin manifests, and both marketplace version fields [@plugin-tests]. The install docs describe Claude Code and Codex marketplace installation separately but state that both use the same npm-backed plugin package [@install-doc]. @@ -57,7 +81,15 @@ The marketplace catalog under `.claude-plugin/marketplace.json` lists one plugin The same executable has an internal `--rudder-event` mode for the five validated events in `src/rudder-telemetry.ts` [@hook-bin] [@rudder-telemetry]. `skills/rudder/scripts/telemetry.mjs` launches that bundled artifact as a detached best-effort child for run, context, backup, question, and completion metadata without exposing PostHog to the skill process [@skill-telemetry]. Both modes catch top-level failures, report exceptions best-effort, close any database handle, shut down telemetry, and avoid model-visible output [@hook-bin]. -The `build` script creates the installed runtime artifact by bundling `bin/rudder-prompt-hook.ts` with esbuild for Node ESM output at `dist/rudder-prompt-hook.mjs`, then copying committed Drizzle migrations into `dist/drizzle` [@package-json]. `pretest`, `test:coverage`, and `prepack` build before their downstream work, so tests, coverage, and packed artifacts use a fresh bundle [@package-json]. Plugin package tests enforce matching Claude/Codex metadata, required package file entries including the skill telemetry helper, marketplace npm source fields, hook command shape, and silent prompt-hook execution for both `PLUGIN_ROOT` and `CLAUDE_PLUGIN_ROOT` environments [@plugin-tests]. +The `build` script creates the installed runtime artifacts by bundling `bin/rudder-prompt-hook.ts` and `bin/rudder-mcp-server.ts` with esbuild for Node ESM output, building the MCP App resource, and then copying committed Drizzle migrations into `dist/drizzle` [@package-json]. `pretest`, `test:coverage`, and `prepack` build before their downstream work, so tests, coverage, and packed artifacts use fresh package output [@package-json]. Plugin package tests enforce matching Claude/Codex metadata, required package file entries including the skill telemetry helper and MCP configuration, marketplace npm source fields, hook command shape, and silent prompt-hook execution for both `PLUGIN_ROOT` and `CLAUDE_PLUGIN_ROOT` environments [@plugin-tests]. + +## MCP App Runtime + +The MCP App path uses the same host-neutral plugin-root pattern as the prompt hook. Both plugin manifests point to `./.mcp.json`; that file defines one `rudder` MCP server launched with `node --input-type=module -e` from `cwd: "."` [@claude-manifest] [@codex-manifest] [@mcp-config]. The inline launcher resolves the installed root from `PLUGIN_ROOT`, `CLAUDE_PLUGIN_ROOT`, or the current working directory, then imports `dist/rudder-mcp-server.mjs` from that root [@mcp-config]. The bundled entrypoint calls `runRudderMcpServer()`, whose default transport is stdio, whose plugin-root resolver accepts either host root variable, and whose app loader reads `dist/rudder-app.html` from the installed package [@mcp-bin] [@mcp-runner]. + +`createRudderMcpServer` registers the app resource at `ui://rudder/workspace.html` and exposes a read-only `open_rudder` tool that points its `_meta.ui.resourceUri` at that resource [@mcp-server]. The tool returns structured launch content with a `ready` status, title, summary, and fallback text so clients without a visible app surface still receive a useful result [@mcp-server]. MCP server tests connect through an in-memory transport to assert the tool, resource URI, MIME type, inline app HTML, and structured result, then launch the packaged stdio server once with `PLUGIN_ROOT` and once with `CLAUDE_PLUGIN_ROOT` [@mcp-tests]. + +The browser app is packaged as a single HTML resource. `scripts/build-mcp-app.mjs` bundles `ui/rudder-app.ts` as a browser IIFE, escapes script and HTML-comment delimiters inside the generated script, replaces the `` marker in the HTML template, and writes `dist/rudder-app.html` [@app-builder]. The app source connects to the MCP Apps bridge, renders structured launch results into the `status`, `title`, `summary`, and `fallback` elements, and falls back to chat instructions when the host cannot provide the app bridge [@app-source]. UI tests cover structured-result rendering, absent or malformed result fallback, unavailable-host fallback, and missing-template-element failures [@ui-tests]. ## Release Boundary diff --git a/almanac/guides/contributor/run-checks.md b/almanac/guides/contributor/run-checks.md index a9e0c8a..77495b4 100644 --- a/almanac/guides/contributor/run-checks.md +++ b/almanac/guides/contributor/run-checks.md @@ -1,6 +1,6 @@ --- title: "Run Checks" -summary: "Run checks is the repo-wide validation procedure for branch changes before commit, PR review, or publishing." +summary: "Run checks is the repo-wide validation procedure for branch changes before commit, PR review, or publishing, including Claude/Codex provider parity for plugin-facing work." topics: [guides, contributor-workflow, validation] sources: - id: check-skill @@ -19,7 +19,7 @@ sources: # Run Checks -Run checks when a branch is ready for commit, PR review, or publishing validation. Rudder is one repo-wide plugin package, so the local check flow identifies branch and working-tree changes, verifies the centralized agent-instruction layout, verifies agent attribution, installs dependencies when needed, runs TypeScript, test, and build commands, and surfaces open PR comments before reporting pass, fail, pending, or skip status [@check-skill]. For background on how this guide fits the automation system, see [Contributor Automation](../../architecture/automation/contributor-automation), [Package Scripts](../../reference/tooling/package-scripts), and [GitHub Workflows](../../reference/automation/github-workflows). +Run checks when a branch is ready for commit, PR review, or publishing validation. Rudder is one repo-wide plugin package, so the local check flow identifies branch and working-tree changes, verifies Claude/Codex provider parity when provider-facing surfaces change, verifies the centralized agent-instruction layout, verifies agent attribution, installs dependencies when needed, runs TypeScript, test, and build commands, and surfaces open PR comments before reporting pass, fail, pending, or skip status [@check-skill]. For background on how this guide fits the automation system, see [Contributor Automation](../../architecture/automation/contributor-automation), [Package Scripts](../../reference/tooling/package-scripts), and [GitHub Workflows](../../reference/automation/github-workflows). ## Start From The Branch Diff @@ -32,7 +32,7 @@ git diff --name-only git diff --name-only --cached ``` -The check skill treats changes under `src/`, `bin/`, `test/`, package and TypeScript config files, and `.github/` as reasons to run the package checks [@check-skill]. If nothing relevant changed, report that no package checks were required instead of manufacturing a local test run [@check-skill]. +The check skill treats changes under `src/`, `bin/`, `test/`, `ui/`, `.claude-plugin/`, `.codex-plugin/`, `hooks/`, `skills/`, package and TypeScript config files, `.mcp.json`, and `.github/` as reasons to run the package checks [@check-skill]. If nothing relevant changed, report that no package checks were required instead of manufacturing a local test run [@check-skill]. ## Check Agent Instruction Layout @@ -40,6 +40,14 @@ Before running package commands, verify the centralized agent-instruction layout The package script `check:agent-layout` enforces the symlinks, verifies that `.claude/commands` does not exist, and checks that `CLAUDE.md` contains the `@AGENTS.md` handoff [@package]. If a link is missing or resolves outside `.agents/`, mark the check as failed and report the broken path [@check-skill]. +## Verify Provider Parity + +Provider-facing work must preserve Claude and Codex as supported providers for the same Rudder plugin package [@check-skill]. When a branch changes manifest metadata, skills, hooks, MCP servers, apps, UI, or packaged artifacts, inspect both provider manifests and runtime paths before running package commands [@check-skill]. + +The parity gate is behavioral. Discovery, launch behavior, packaged resources, and user-facing capability must be equivalent across providers; schema-specific fields or environment-variable differences are allowed only when they remain implementation details rather than functional gaps [@check-skill]. Add or update a regression assertion in `test/plugin-package.test.ts` or the relevant runtime test so both provider paths are exercised; provider-facing behavior without parity coverage fails the check [@check-skill]. + +Provider-specific exceptions require an explicit user request or an unsupported provider capability. Report the affected provider, the reason, and the test that protects the supported behavior [@check-skill]. + ## Verify Agent Attribution For committed agent-written work, inspect `git log origin/main..HEAD` and confirm every coding agent appears as a commit author or in a `Co-authored-by` trailer [@check-skill]. Missing attribution fails the check. When agent-written changes are still uncommitted, report attribution as pending and add the identifying trailer when committing; human-only changes are outside this gate [@check-skill]. @@ -62,4 +70,4 @@ If the current branch has an open GitHub PR, run the [Address PR Comments](addre ## Report The Result -The final report should include separate status lines for agent-instruction layout, agent attribution, typecheck, tests, build, and PR comments [@check-skill]. For failures, include the failing command and the key error output; distinguish real failures such as broken agent links, missing agent attribution, type errors, test failures, or unaddressed high-priority comments from environment issues such as missing CLI tools or no PR [@check-skill]. +The final report should include separate status lines for provider parity, agent-instruction layout, agent attribution, typecheck, tests, build, and PR comments [@check-skill]. For failures, include the failing command and the key error output; distinguish real failures such as provider drift, missing parity coverage, broken agent links, missing agent attribution, type errors, test failures, or unaddressed high-priority comments from environment issues such as missing CLI tools or no PR [@check-skill]. diff --git a/almanac/reference/tooling/package-scripts.md b/almanac/reference/tooling/package-scripts.md index 08c4593..6e7267c 100644 --- a/almanac/reference/tooling/package-scripts.md +++ b/almanac/reference/tooling/package-scripts.md @@ -1,11 +1,17 @@ --- title: "Package Scripts Reference" summary: "This reference documents Rudder's npm scripts and the automation paths that call them." -topics: [reference, tooling, package, validation] +topics: [reference, tooling, package, mcp-app, validation] sources: - id: package-json type: file path: package.json + - id: mcp-config + type: file + path: .mcp.json + - id: mcp-runner + type: file + path: src/mcp/run-server.ts - id: db-client type: file path: src/db/client.ts @@ -32,8 +38,8 @@ This reference lists the npm scripts defined by Rudder's package and the local o | `danger:ci` | `danger ci --failOnErrors` | Runs Danger with failing errors for CI agent-guard enforcement [@package-json]. | | `check:agent-layout` | `test -L .claude/skills && test -L .codex/skills && test .claude/skills -ef .agents/skills && test .codex/skills -ef .agents/skills && test ! -e .claude/commands && grep -Fxq '@AGENTS.md' CLAUDE.md` | Verifies Claude/Codex skill symlinks, absence of Claude command aliases, and the `CLAUDE.md` handoff [@package-json]. | | `typecheck` | `tsc --noEmit` | Runs TypeScript checking without writing build output [@package-json]. | -| `build` | `rm -rf dist && esbuild bin/rudder-prompt-hook.ts --bundle --platform=node --format=esm --target=node24 --outfile=dist/rudder-prompt-hook.mjs && cp -R drizzle dist/drizzle` | Removes old `dist` output, bundles the prompt-capture and telemetry entrypoint for Node ESM, then copies generated Drizzle migrations into the package build tree [@package-json]. | -| `pretest` | `npm run build` | Rebuilds the hook bundle before tests [@package-json]. | +| `build` | `rm -rf dist && esbuild bin/rudder-prompt-hook.ts --bundle --platform=node --format=esm --target=node24 --outfile=dist/rudder-prompt-hook.mjs && esbuild bin/rudder-mcp-server.ts --bundle --platform=node --format=esm --target=node24 --outfile=dist/rudder-mcp-server.mjs && node scripts/build-mcp-app.mjs && cp -R drizzle dist/drizzle` | Removes old `dist` output, bundles the prompt hook and MCP server for Node ESM, builds the single-file MCP App HTML resource, then copies generated Drizzle migrations into the package build tree [@package-json]. | +| `pretest` | `npm run build` | Rebuilds package artifacts before tests [@package-json]. | | `test` | `node --test` | Runs Node's built-in test runner [@package-json]. | | `test:coverage` | `npm run build && c8 node --test && diff-cover coverage/lcov.info --fail-under=90 --show-uncovered --include-untracked` | Builds first, runs the full Node suite under c8, writes LCOV, and fails when changed or untracked source lines are below 90% coverage [@package-json]. | | `prepack` | `npm run build` | Rebuilds package artifacts before `npm pack` [@package-json]. | @@ -41,10 +47,10 @@ This reference lists the npm scripts defined by Rudder's package and the local o ## Automation Consumers -The Test workflow checks out full Git history, installs dependencies with `npm ci`, then runs `npm run check:agent-layout`, `npm run format:markdown:check`, `npm run typecheck`, `npm run test:coverage`, and `npm run build` in that order [@test-workflow]. Full history is required because `diff-cover` compares LCOV results with changed lines from Git [@test-workflow] [@package-json]. The local check skill remains the faster branch gate: it runs `npm run typecheck`, `npm test`, and `npm run build` after enforcing the centralized agent-instruction layout, verifying agent attribution, and installing dependencies when `node_modules/` is missing [@check-skill]. +The Test workflow checks out full Git history, installs dependencies with `npm ci`, then runs `npm run check:agent-layout`, `npm run format:markdown:check`, `npm run typecheck`, `npm run test:coverage`, and `npm run build` in that order [@test-workflow]. Full history is required because `diff-cover` compares LCOV results with changed lines from Git [@test-workflow] [@package-json]. The local check skill remains the faster branch gate: it runs `npm run typecheck`, `npm test`, and `npm run build` after enforcing provider parity for provider-facing work, enforcing the centralized agent-instruction layout, verifying agent attribution, and installing dependencies when `node_modules/` is missing [@check-skill]. -The c8 configuration measures `bin/**/*.ts`, `skills/**/*.mjs`, and `src/**/*.ts`, excludes `dist/**` and `test/**`, enables `all`, and emits the LCOV report consumed by `diff-cover` [@package-json]. `prepublishOnly` itself relies on the ordinary npm test lifecycle for the build, and `prepack` rebuilds again before packaging [@package-json]. The publish workflow separately runs `test:coverage` before packaging, so release automation enforces the changed-line threshold even though `npm publish`'s lifecycle hook uses `npm test` [@publish-workflow] [@package-json]. The `build` copy step is part of the database runtime contract because the installed prompt hook points `RUDDER_MIGRATIONS_PATH` at `dist/drizzle`; the decision is recorded in [Generated Drizzle Migrations](../../decisions/database/generated-drizzle-migrations) [@db-client]. The release workflow behavior is covered from the GitHub Actions side in the [GitHub Workflows](../automation/github-workflows) reference, release preparation is covered in [Prepare Package Release](../../guides/release/prepare-package-release), and the contributor-facing procedure is covered in [Run Checks](../../guides/contributor/run-checks). +The c8 configuration measures `bin/**/*.ts`, `skills/**/*.mjs`, `src/**/*.ts`, and `ui/**/*.ts`, excludes `dist/**` and `test/**`, enables `all`, and emits the LCOV report consumed by `diff-cover` [@package-json]. `prepublishOnly` itself relies on the ordinary npm test lifecycle for the build, and `prepack` rebuilds again before packaging [@package-json]. The publish workflow separately runs `test:coverage` before packaging, so release automation enforces the changed-line threshold even though `npm publish`'s lifecycle hook uses `npm test` [@publish-workflow] [@package-json]. The `build` copy step is part of the database runtime contract because the installed prompt hook points `RUDDER_MIGRATIONS_PATH` at `dist/drizzle`; the decision is recorded in [Generated Drizzle Migrations](../../decisions/database/generated-drizzle-migrations) [@db-client]. The same build script is also part of the MCP App package contract because `.mcp.json` loads `dist/rudder-mcp-server.mjs`, and that server reads `dist/rudder-app.html` from the installed package [@package-json] [@mcp-config] [@mcp-runner]. The release workflow behavior is covered from the GitHub Actions side in the [GitHub Workflows](../automation/github-workflows) reference, release preparation is covered in [Prepare Package Release](../../guides/release/prepare-package-release), and the contributor-facing procedure is covered in [Run Checks](../../guides/contributor/run-checks). ## Change Surface -Changes to `typecheck`, `test`, or `build` affect local checks, CI, and package publication, while changes to `test:coverage` or the c8 configuration affect the CI and publish changed-line gate [@package-json] [@test-workflow] [@check-skill]. Changes to `build` can also affect runtime prompt capture and Rudder usage telemetry if packaged output no longer includes `dist/rudder-prompt-hook.mjs` or `dist/drizzle` [@package-json]. Changes to `db:generate` affect migration-generation work and should be checked against the runtime migration decision [@package-json]. +Changes to `typecheck`, `test`, or `build` affect local checks, CI, and package publication, while changes to `test:coverage` or the c8 configuration affect the CI and publish changed-line gate [@package-json] [@test-workflow] [@check-skill]. Provider-facing changes to manifests, hooks, skills, MCP servers, apps, UI, or packaged artifacts must also preserve Claude/Codex parity and parity coverage through the local check skill [@check-skill]. Changes to `build` can affect runtime prompt capture and Rudder usage telemetry if packaged output no longer includes `dist/rudder-prompt-hook.mjs` or `dist/drizzle`, and can affect MCP App startup if packaged output no longer includes `dist/rudder-mcp-server.mjs` or `dist/rudder-app.html` [@package-json] [@mcp-config]. Changes to `db:generate` affect migration-generation work and should be checked against the runtime migration decision [@package-json]. diff --git a/almanac/reference/tooling/typescript-build.md b/almanac/reference/tooling/typescript-build.md index 1564736..cca1245 100644 --- a/almanac/reference/tooling/typescript-build.md +++ b/almanac/reference/tooling/typescript-build.md @@ -1,7 +1,7 @@ --- title: "TypeScript And Bundle Build" -summary: "This reference records Rudder's no-emit TypeScript checking contract and esbuild bundle output for the plugin prompt-capture and telemetry runtime." -topics: [reference, typescript, tooling, package, plugin] +summary: "This reference records Rudder's no-emit TypeScript checking contract and generated build output for the prompt-capture and telemetry runtime, MCP server, and MCP App UI resource." +topics: [reference, typescript, tooling, package, plugin, mcp-app] sources: - id: tsconfig type: file @@ -12,12 +12,21 @@ sources: - id: hook-bin type: file path: bin/rudder-prompt-hook.ts + - id: mcp-bin + type: file + path: bin/rudder-mcp-server.ts + - id: app-builder + type: file + path: scripts/build-mcp-app.mjs + - id: app-source + type: file + path: ui/rudder-app.ts - id: publish-workflow type: file path: .github/workflows/publish.yml --- -This reference defines Rudder's current TypeScript and bundle contract. `tsconfig.json` is a strict, no-emit NodeNext setup for `bin/**/*.ts`, `dangerfile.ts`, and `src/**/*.ts`; `npm run build` uses esbuild to bundle `bin/rudder-prompt-hook.ts` into `dist/rudder-prompt-hook.mjs` and then copies `drizzle/` into `dist/drizzle` [@tsconfig] [@package-json] [@hook-bin]. The bundled entrypoint handles both host prompt hooks and internal Rudder usage events [@hook-bin]. The package manifest declares the package as ESM, requires Node `>=24.0.0`, and wires `typecheck`, `build`, `pretest`, `test:coverage`, `prepack`, and `prepublishOnly` to that contract [@package-json]. +This reference defines Rudder's current TypeScript and generated build contract. `tsconfig.json` is a strict, no-emit NodeNext setup for `bin/**/*.ts`, `dangerfile.ts`, `src/**/*.ts`, and `ui/**/*.ts`; `npm run build` uses esbuild to bundle `bin/rudder-prompt-hook.ts` into `dist/rudder-prompt-hook.mjs`, bundle `bin/rudder-mcp-server.ts` into `dist/rudder-mcp-server.mjs`, generate `dist/rudder-app.html` from the UI source, and then copy `drizzle/` into `dist/drizzle` [@tsconfig] [@package-json] [@hook-bin] [@mcp-bin] [@app-builder]. The prompt-hook entrypoint handles both host prompt hooks and internal Rudder usage events [@hook-bin]. The package manifest declares the package as ESM, requires Node `>=24.0.0`, and wires `typecheck`, `build`, `pretest`, `test:coverage`, `prepack`, and `prepublishOnly` to that contract [@package-json]. ## Package Context @@ -49,13 +58,13 @@ For the architecture-level explanation of this package boundary, see [package ba | `strict` | `true` [@tsconfig] | | `skipLibCheck` | `true` [@tsconfig] | | `types` | `["node"]` [@tsconfig] | -| `lib` | `["ES2023"]` [@tsconfig] | +| `lib` | `["ES2023", "DOM"]` [@tsconfig] | -The NodeNext settings make the compiler follow Node's ESM-aware module rules, while `allowImportingTsExtensions` and `rewriteRelativeImportExtensions` allow TypeScript source imports to name `.ts` files and rewrite relative import extensions during emit [@tsconfig]. `verbatimModuleSyntax` keeps the written import and export syntax as the source of truth for module form [@tsconfig]. +The NodeNext settings make the compiler follow Node's ESM-aware module rules, while `allowImportingTsExtensions` and `rewriteRelativeImportExtensions` allow TypeScript source imports to name `.ts` files and rewrite relative import extensions during emit [@tsconfig]. `verbatimModuleSyntax` keeps the written import and export syntax as the source of truth for module form, and the `DOM` library is required because the MCP App UI source uses browser `document` and element types [@tsconfig] [@app-source]. ## Bundle Output -The repository no longer has a `tsconfig.build.json` overlay. Build output is generated by esbuild from `bin/rudder-prompt-hook.ts` with `--bundle`, `--platform=node`, `--format=esm`, `--target=node24`, and `--outfile=dist/rudder-prompt-hook.mjs` [@package-json] [@hook-bin]. The build then copies `drizzle/` into `dist/drizzle` so the installed runtime can point prompt-capture migration lookup at packaged migration files [@package-json] [@hook-bin]. +The repository no longer has a `tsconfig.build.json` overlay. Node build output is generated by esbuild from `bin/rudder-prompt-hook.ts` and `bin/rudder-mcp-server.ts` with `--bundle`, `--platform=node`, `--format=esm`, and `--target=node24`, producing `dist/rudder-prompt-hook.mjs` and `dist/rudder-mcp-server.mjs` [@package-json] [@hook-bin] [@mcp-bin]. The browser app output is generated by `scripts/build-mcp-app.mjs`, which bundles `ui/rudder-app.ts` as an IIFE, injects the escaped script into the app template marker, and writes `dist/rudder-app.html` [@package-json] [@app-builder] [@app-source]. The build then copies `drizzle/` into `dist/drizzle` so the installed prompt hook can point migration lookup at packaged migration files [@package-json]. ## Included Sources @@ -66,9 +75,10 @@ The TypeScript config includes these source globs [@tsconfig]. | `bin/**/*.ts` | `tsconfig.json` [@tsconfig]. | | `dangerfile.ts` | `tsconfig.json` [@tsconfig]. | | `src/**/*.ts` | `tsconfig.json` [@tsconfig]. | +| `ui/**/*.ts` | `tsconfig.json` [@tsconfig]. | -No test glob is included in the TypeScript config, so package typechecking is scoped to the hook entrypoint, runtime sources, and Danger policy file [@tsconfig]. +No test glob is included in the TypeScript config, so package typechecking is scoped to executable entrypoints, runtime sources, UI source, and the Danger policy file [@tsconfig]. ## Consuming Scripts -`typecheck` runs `tsc --noEmit`, matching the config's no-emit intent [@package-json] [@tsconfig]. `build` removes `dist`, bundles the runtime entrypoint with esbuild, and copies `drizzle/` into `dist/drizzle` [@package-json]. `test:coverage` also starts with a build before running the full suite under c8 and checking changed lines with `diff-cover` [@package-json]. `pretest` and `prepack` both run `npm run build`, while `prepublishOnly` runs `npm run typecheck && npm test`; because `npm test` triggers `pretest`, the npm lifecycle depends on typecheck, a fresh bundle, and the test suite, while the publish workflow adds the explicit changed-line coverage gate [@package-json] [@publish-workflow]. +`typecheck` runs `tsc --noEmit`, matching the config's no-emit intent [@package-json] [@tsconfig]. `build` removes `dist`, bundles the prompt hook and MCP server, builds the app HTML resource, and copies `drizzle/` into `dist/drizzle` [@package-json]. `test:coverage` also starts with a build before running the full suite under c8 and checking changed lines with `diff-cover` [@package-json]. `pretest` and `prepack` both run `npm run build`, while `prepublishOnly` runs `npm run typecheck && npm test`; because `npm test` triggers `pretest`, the npm lifecycle depends on typecheck, fresh generated package output, and the test suite, while the publish workflow adds the explicit changed-line coverage gate [@package-json] [@publish-workflow]. diff --git a/almanac/topics.yaml b/almanac/topics.yaml index 586bfe9..67f0879 100644 --- a/almanac/topics.yaml +++ b/almanac/topics.yaml @@ -43,6 +43,10 @@ topics: title: Local State description: User-scoped runtime state such as the Rudder home directory, SQLite database, and telemetry identity. parents: [runtime] + - slug: mcp-app + title: MCP App + description: MCP server, app resource, UI launcher, and host manifest configuration for Rudder's interactive UI. + parents: [plugin, runtime] - slug: package title: Package description: npm package metadata, exports, scripts, publishing, and build outputs. diff --git a/bin/rudder-mcp-server.ts b/bin/rudder-mcp-server.ts new file mode 100644 index 0000000..1b7a2ac --- /dev/null +++ b/bin/rudder-mcp-server.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { runRudderMcpServer } from '../src/mcp/run-server.ts'; + +await runRudderMcpServer(); diff --git a/package-lock.json b/package-lock.json index f91d522..c4dbbdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.4", "license": "Apache-2.0", "devDependencies": { + "@modelcontextprotocol/ext-apps": "^1.7.5", + "@modelcontextprotocol/sdk": "^1.30.0", "@types/node": "^24", "c8": "^12.0.0", "danger": "^13.0.10", @@ -18,7 +20,8 @@ "esbuild": "^0.25.12", "posthog-node": "^5.41.0", "rumdl": "0.2.41", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "zod": "^4.4.3" }, "engines": { "node": ">=24.0.0" @@ -526,6 +529,19 @@ "node": ">=18.0.0" } }, + "node_modules/@hono/node-server": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", @@ -584,6 +600,77 @@ "node": ">=12" } }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.5.tgz", + "integrity": "sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "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/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -899,6 +986,13 @@ "node": ">=18.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/commander": { "version": "2.12.5", "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.5.tgz", @@ -983,6 +1077,55 @@ "dev": true, "license": "MIT" }, + "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/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": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1046,6 +1189,45 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/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/brace-expansion": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", @@ -1079,6 +1261,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "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/c8": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", @@ -1231,6 +1423,30 @@ "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/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1238,6 +1454,26 @@ "dev": true, "license": "MIT" }, + "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/core-js": { "version": "3.49.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", @@ -1250,6 +1486,24 @@ "url": "https://opencollective.com/core-js" } }, + "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", @@ -1337,6 +1591,16 @@ } } }, + "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/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -1624,6 +1888,13 @@ "safe-buffer": "^5.0.1" } }, + "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/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -1631,6 +1902,16 @@ "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", @@ -1716,6 +1997,117 @@ "node": ">=6" } }, + "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/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.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.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.6.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", + "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "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-json-patch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", @@ -1723,6 +2115,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1736,6 +2145,28 @@ "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", @@ -1770,6 +2201,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1932,6 +2383,16 @@ "node": ">=12.0.0" } }, + "node_modules/hono": { + "version": "4.12.32", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", + "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -1939,6 +2400,27 @@ "dev": true, "license": "MIT" }, + "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/hyperlinker": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", @@ -1949,6 +2431,30 @@ "node": ">=4" } }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "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/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/ini": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", @@ -1959,6 +2465,26 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/ip-address": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", + "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + "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-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1969,6 +2495,13 @@ "node": ">=0.12.0" } }, + "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", @@ -2025,6 +2558,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.5.tgz", + "integrity": "sha512-2E5L2yRp03FnwreJLJX8/r7mHiZICCf8kG7fAsTWkSQTDAcc46NIZoQLKy+EJ8sPoJlxyS4OQR5H70LjIZZlIQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jsbi": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", @@ -2042,6 +2585,20 @@ "node": ">=0.1.90" } }, + "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/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2233,6 +2790,20 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/memfs-or-file-map-to-github-branch": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/memfs-or-file-map-to-github-branch/-/memfs-or-file-map-to-github-branch-1.3.0.tgz", @@ -2243,6 +2814,19 @@ "@octokit/rest": "*" } }, + "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/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -2257,6 +2841,33 @@ "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": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -2300,6 +2911,16 @@ "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/node-cleanup": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", @@ -2343,6 +2964,16 @@ "node": ">= 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", @@ -2356,6 +2987,19 @@ "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", @@ -2435,6 +3079,16 @@ "xtend": "~4.0.1" } }, + "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", @@ -2472,6 +3126,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", @@ -2492,6 +3157,16 @@ "dev": true, "license": "MIT" }, + "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/posthog-node": { "version": "5.46.0", "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.46.0.tgz", @@ -2527,6 +3202,20 @@ "prettyjson": "bin/prettyjson" } }, + "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/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -2544,6 +3233,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/readline-sync": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", @@ -2591,6 +3310,23 @@ "node": ">= 4" } }, + "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/rumdl": { "version": "0.2.41", "resolved": "https://registry.npmjs.org/rumdl/-/rumdl-0.2.41.tgz", @@ -2634,6 +3370,13 @@ ], "license": "MIT" }, + "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.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -2647,6 +3390,60 @@ "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", @@ -2759,6 +3556,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/string-width": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", @@ -2876,6 +3683,49 @@ "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/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", @@ -2914,6 +3764,16 @@ "dev": true, "license": "ISC" }, + "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/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -2929,6 +3789,16 @@ "node": ">=10.12.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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3088,6 +3958,26 @@ "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/package.json b/package.json index 37fe258..532d560 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "node": ">=24.0.0" }, "files": [ + ".mcp.json", + ".claude-mcp.json", ".claude-plugin", ".codex-plugin", "assets", @@ -45,7 +47,7 @@ "danger:ci": "danger ci --failOnErrors", "check:agent-layout": "test -L .claude/skills && test -L .codex/skills && test .claude/skills -ef .agents/skills && test .codex/skills -ef .agents/skills && test ! -e .claude/commands && grep -Fxq '@AGENTS.md' CLAUDE.md", "typecheck": "tsc --noEmit", - "build": "rm -rf dist && esbuild bin/rudder-prompt-hook.ts --bundle --platform=node --format=esm --target=node24 --outfile=dist/rudder-prompt-hook.mjs && cp -R drizzle dist/drizzle", + "build": "rm -rf dist && esbuild bin/rudder-prompt-hook.ts --bundle --platform=node --format=esm --target=node24 --outfile=dist/rudder-prompt-hook.mjs && esbuild bin/rudder-mcp-server.ts --bundle --platform=node --format=esm --target=node24 --outfile=dist/rudder-mcp-server.mjs && node scripts/build-mcp-app.mjs && cp -R drizzle dist/drizzle", "pretest": "npm run build", "test": "node --test", "test:coverage": "npm run build && c8 node --test && diff-cover coverage/lcov.info --fail-under=90 --show-uncovered --include-untracked", @@ -57,7 +59,8 @@ "include": [ "bin/**/*.ts", "skills/**/*.mjs", - "src/**/*.ts" + "src/**/*.ts", + "ui/**/*.ts" ], "exclude": [ "dist/**", @@ -68,6 +71,8 @@ ] }, "devDependencies": { + "@modelcontextprotocol/ext-apps": "^1.7.5", + "@modelcontextprotocol/sdk": "^1.30.0", "@types/node": "^24", "c8": "^12.0.0", "danger": "^13.0.10", @@ -77,7 +82,8 @@ "esbuild": "^0.25.12", "posthog-node": "^5.41.0", "rumdl": "0.2.41", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "zod": "^4.4.3" }, "overrides": { "danger": { diff --git a/scripts/build-mcp-app.mjs b/scripts/build-mcp-app.mjs new file mode 100644 index 0000000..8fb5a06 --- /dev/null +++ b/scripts/build-mcp-app.mjs @@ -0,0 +1,35 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { build } from 'esbuild'; + +const pluginRoot = fileURLToPath(new URL('../', import.meta.url)); +const marker = ''; +const template = await readFile( + join(pluginRoot, 'ui', 'rudder-app.html'), + 'utf8' +); +const result = await build({ + entryPoints: [join(pluginRoot, 'ui', 'rudder-app.ts')], + bundle: true, + format: 'iife', + minify: true, + platform: 'browser', + target: 'es2022', + write: false, +}); +const output = result.outputFiles?.[0]; + +if (!output || !template.includes(marker)) { + throw new Error('Could not build the Rudder MCP App resource'); +} + +const script = output.text + .replaceAll('${script}` +); + +await writeFile(join(pluginRoot, 'dist', 'rudder-app.html'), html); diff --git a/src/mcp/run-server.ts b/src/mcp/run-server.ts new file mode 100644 index 0000000..122bf7b --- /dev/null +++ b/src/mcp/run-server.ts @@ -0,0 +1,47 @@ +import { readFile } from 'node:fs/promises'; +import { resolve, sep } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import { createRudderMcpServer } from './server.ts'; + +interface RunRudderMcpServerOptions { + pluginRoot?: URL; + transport?: Transport; +} + +export function resolvePluginRoot( + env: NodeJS.ProcessEnv = process.env, + moduleUrl: string = import.meta.url +): URL { + const configuredRoot = env.PLUGIN_ROOT ?? env.CLAUDE_PLUGIN_ROOT; + return configuredRoot + ? pathToFileURL(`${resolve(configuredRoot)}${sep}`) + : new URL('../', moduleUrl); +} + +async function packageVersion(pluginRoot: URL): Promise { + try { + const manifest = JSON.parse( + await readFile(new URL('package.json', pluginRoot), 'utf8') + ) as Record; + return typeof manifest.version === 'string' && manifest.version + ? manifest.version + : 'unknown'; + } catch { + return 'unknown'; + } +} + +export async function runRudderMcpServer({ + pluginRoot = resolvePluginRoot(), + transport = new StdioServerTransport(), +}: RunRudderMcpServerOptions = {}) { + const server = createRudderMcpServer({ + version: await packageVersion(pluginRoot), + loadAppHtml: () => + readFile(new URL('dist/rudder-app.html', pluginRoot), 'utf8'), + }); + await server.connect(transport); + return server; +} diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 0000000..7f6f658 --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,89 @@ +import { + registerAppResource, + registerAppTool, + RESOURCE_MIME_TYPE, +} from '@modelcontextprotocol/ext-apps/server'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; + +export const RUDDER_APP_RESOURCE_URI = 'ui://rudder/workspace.html'; + +interface RudderMcpServerOptions { + loadAppHtml(): Promise; + version: string; +} + +const launchResult = { + status: 'ready' as const, + title: 'Rudder', + summary: 'Turn coding-session intent into focused tests.', + fallback: + 'Use the Rudder skill to generate and verify tests for the current branch.', +}; + +export function createRudderMcpServer({ + loadAppHtml, + version, +}: RudderMcpServerOptions): McpServer { + const server = new McpServer({ + name: 'rudder', + version, + }); + + registerAppResource( + server, + 'Rudder workspace', + RUDDER_APP_RESOURCE_URI, + { + description: 'Interactive workspace for the Rudder test workflow.', + }, + async () => ({ + contents: [ + { + uri: RUDDER_APP_RESOURCE_URI, + mimeType: RESOURCE_MIME_TYPE, + text: await loadAppHtml(), + }, + ], + }) + ); + + registerAppTool( + server, + 'open_rudder', + { + title: 'Open Rudder', + description: + 'Open the Rudder workspace. Use it when the user asks to view or work with Rudder in an interactive UI.', + inputSchema: {}, + outputSchema: { + status: z.literal('ready'), + title: z.string(), + summary: z.string(), + fallback: z.string(), + }, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + _meta: { + ui: { + resourceUri: RUDDER_APP_RESOURCE_URI, + }, + }, + }, + async () => ({ + content: [ + { + type: 'text', + text: `${launchResult.summary} ${launchResult.fallback}`, + }, + ], + structuredContent: launchResult, + }) + ); + + return server; +} diff --git a/test/mcp-server.test.ts b/test/mcp-server.test.ts new file mode 100644 index 0000000..023455e --- /dev/null +++ b/test/mcp-server.test.ts @@ -0,0 +1,131 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { test } from 'node:test'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { + resolvePluginRoot, + runRudderMcpServer, +} from '../src/mcp/run-server.ts'; +import { RUDDER_APP_RESOURCE_URI } from '../src/mcp/server.ts'; + +const pluginRoot = fileURLToPath(new URL('../', import.meta.url)); + +// codex/019fb444-8493-7973-9439-341f7b35ed2a/019fb444-892e-7462-946a-9ad14220edcc +test('serves a headless-safe tool and its bundled MCP App resource', async (context) => { + const client = new Client({ + name: 'rudder-test-client', + version: '1.0.0', + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + const server = await runRudderMcpServer({ + pluginRoot: pathToFileURL(`${pluginRoot}/`), + transport: serverTransport, + }); + context.after(async () => server.close()); + context.after(async () => client.close()); + + await client.connect(clientTransport); + + const tools = await client.listTools(); + const openRudder = tools.tools.find((tool) => tool.name === 'open_rudder'); + assert.ok(openRudder); + assert.equal( + (openRudder._meta?.ui as { resourceUri?: unknown } | undefined) + ?.resourceUri, + RUDDER_APP_RESOURCE_URI + ); + assert.equal(openRudder.annotations?.readOnlyHint, true); + + const result = await client.callTool({ + name: 'open_rudder', + arguments: {}, + }); + assert.equal(result.isError, undefined); + assert.deepEqual(result.structuredContent, { + status: 'ready', + title: 'Rudder', + summary: 'Turn coding-session intent into focused tests.', + fallback: + 'Use the Rudder skill to generate and verify tests for the current branch.', + }); + assert.match(result.content[0]?.text ?? '', /Use the Rudder skill/); + + const resources = await client.listResources(); + assert.ok( + resources.resources.some( + (resource) => resource.uri === RUDDER_APP_RESOURCE_URI + ) + ); + + const resource = await client.readResource({ + uri: RUDDER_APP_RESOURCE_URI, + }); + const appHtml = resource.contents[0]; + assert.equal(appHtml?.mimeType, 'text/html;profile=mcp-app'); + assert.ok('text' in appHtml); + assert.match(appHtml.text, /data-rudder-app/); + assert.match(appHtml.text, /Turn coding-session intent into focused tests/); + assert.doesNotMatch(appHtml.text, /]+src=/); +}); + +// codex/019fb444-8493-7973-9439-341f7b35ed2a/019fb45d-2e35-73a0-a420-e73a312bb8b8 +test('starts the packaged server for Codex and Claude', async (context) => { + type McpServerConfig = { + command: string; + args: string[]; + cwd: string; + }; + const claudeMcp = JSON.parse( + readFileSync(join(pluginRoot, '.claude-mcp.json'), 'utf8') + ) as { + mcpServers: { + rudder: McpServerConfig; + }; + }; + const codexMcp = JSON.parse( + readFileSync(join(pluginRoot, '.mcp.json'), 'utf8') + ) as { + rudder: McpServerConfig; + }; + const inheritedEnvironment = Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => entry[1] !== undefined + ) + ); + + for (const [hostRoot, config] of [ + ['PLUGIN_ROOT', codexMcp.rudder], + ['CLAUDE_PLUGIN_ROOT', claudeMcp.mcpServers.rudder], + ] as const) { + const client = new Client({ + name: `rudder-${hostRoot.toLowerCase()}-test-client`, + version: '1.0.0', + }); + const environment = { + ...inheritedEnvironment, + [hostRoot]: pluginRoot, + }; + delete environment[ + hostRoot === 'PLUGIN_ROOT' ? 'CLAUDE_PLUGIN_ROOT' : 'PLUGIN_ROOT' + ]; + const transport = new StdioClientTransport({ + command: config.command, + args: config.args, + cwd: join(pluginRoot, config.cwd), + env: environment, + stderr: 'pipe', + }); + context.after(async () => client.close()); + + await client.connect(transport); + + const tools = await client.listTools(); + assert.ok(tools.tools.some((tool) => tool.name === 'open_rudder')); + await client.close(); + } +}); diff --git a/test/plugin-package.test.ts b/test/plugin-package.test.ts index 760c1c6..653c1aa 100644 --- a/test/plugin-package.test.ts +++ b/test/plugin-package.test.ts @@ -40,6 +40,7 @@ after(() => { rmSync(root, { recursive: true, force: true }); }); +// codex/019fb444-8493-7973-9439-341f7b35ed2a/019fb45d-2e35-73a0-a420-e73a312bb8b8 test('ships matching Codex and Claude plugin metadata', () => { const codex = JSON.parse( readFileSync(join(pluginRoot, '.codex-plugin', 'plugin.json'), 'utf8') @@ -58,14 +59,19 @@ test('ships matching Codex and Claude plugin metadata', () => { assert.equal(packageManifest.engines.node, '>=24.0.0'); assert.equal(packageManifest.dependencies, undefined); assert.equal(packageManifest.workspaces, undefined); - assert.equal(codex.skills, './skills/'); + assert.equal(codex.skills, claude.skills); assert.equal(claude.skills, './skills/'); + assert.equal(claude.mcpServers, './.claude-mcp.json'); + assert.equal(codex.mcpServers, './.mcp.json'); + assert.ok(codex.interface.capabilities.includes('Interactive')); assert.equal(claude.hooks, './hooks/hooks.json'); assert.ok(codex.interface.shortDescription.length <= 30); assert.match(codex.interface.privacyPolicyURL, /^https:\/\//); assert.match(codex.interface.termsOfServiceURL, /^https:\/\//); assert.ok(packageManifest.files.includes('.codex-plugin')); assert.ok(packageManifest.files.includes('.claude-plugin')); + assert.ok(packageManifest.files.includes('.mcp.json')); + assert.ok(packageManifest.files.includes('.claude-mcp.json')); assert.ok(packageManifest.files.includes('assets')); assert.ok(packageManifest.files.includes('docs')); assert.ok(packageManifest.files.includes('hooks')); @@ -73,6 +79,59 @@ test('ships matching Codex and Claude plugin metadata', () => { assert.ok(packageManifest.files.includes('dist')); }); +// codex/019fb444-8493-7973-9439-341f7b35ed2a/019fb45a-bff9-7380-ad29-eae9c4b02ff7 +test('requires coverage for the MCP App UI sources', () => { + const packageManifest = JSON.parse( + readFileSync(join(pluginRoot, 'package.json'), 'utf8') + ); + + assert.ok(packageManifest.c8.include.includes('ui/**/*.ts')); + assert.match(packageManifest.scripts['test:coverage'], /diff-cover/); + assert.match(packageManifest.scripts['test:coverage'], /--fail-under=90/); + assert.match(packageManifest.scripts['test:coverage'], /--include-untracked/); +}); + +// codex/019fb444-8493-7973-9439-341f7b35ed2a/019fb444-892e-7462-946a-9ad14220edcc +test('ships equivalent Claude and Codex MCP server definitions', () => { + const claudeMcp = JSON.parse( + readFileSync(join(pluginRoot, '.claude-mcp.json'), 'utf8') + ); + const codexMcp = JSON.parse( + readFileSync(join(pluginRoot, '.mcp.json'), 'utf8') + ); + + assert.deepEqual(Object.keys(claudeMcp), ['mcpServers']); + assert.deepEqual(Object.keys(codexMcp), ['rudder']); + assert.deepEqual(claudeMcp.mcpServers, codexMcp); +}); + +// codex/019fb444-8493-7973-9439-341f7b35ed2a/019fb444-892e-7462-946a-9ad14220edcc +test('ships the bundled MCP server and single-file app resource', () => { + const mcp = JSON.parse( + readFileSync(join(pluginRoot, '.mcp.json'), 'utf8') + ); + const packageManifest = JSON.parse( + readFileSync(join(pluginRoot, 'package.json'), 'utf8') + ); + const serverPath = join(pluginRoot, 'dist', 'rudder-mcp-server.mjs'); + const appPath = join(pluginRoot, 'dist', 'rudder-app.html'); + + assert.deepEqual(Object.keys(mcp), ['rudder']); + assert.equal(mcp.rudder.command, 'node'); + assert.equal(mcp.rudder.cwd, '.'); + assert.deepEqual(mcp.rudder.args.slice(0, 2), [ + '--input-type=module', + '-e', + ]); + assert.match(mcp.rudder.args[2], /PLUGIN_ROOT/); + assert.match(mcp.rudder.args[2], /CLAUDE_PLUGIN_ROOT/); + assert.match(mcp.rudder.args[2], /rudder-mcp-server/); + assert.match(packageManifest.scripts.build, /rudder-mcp-server/); + assert.match(packageManifest.scripts.build, /build-mcp-app/); + assert.ok(readFileSync(serverPath, 'utf8').length > 0); + assert.ok(readFileSync(appPath, 'utf8').length > 0); +}); + test('keeps the Rudder package version synchronized across the codebase', () => { const packageManifest = JSON.parse( readFileSync(join(pluginRoot, 'package.json'), 'utf8') diff --git a/test/ui.test.ts b/test/ui.test.ts new file mode 100644 index 0000000..e52ae4f --- /dev/null +++ b/test/ui.test.ts @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { + startRudderApp, + type RudderAppBridge, + type RudderAppDocument, +} from '../ui/rudder-app.ts'; + +interface UiHarness { + app: RudderAppBridge; + elements: Record; + root: RudderAppDocument; +} + +function harness(connect: () => Promise = async () => {}): UiHarness { + const elements = Object.fromEntries( + ['status', 'title', 'summary', 'fallback'].map((id) => [ + id, + { textContent: null }, + ]) + ); + return { + app: { + connect, + ontoolresult: undefined, + } as unknown as RudderAppBridge, + elements, + root: { + getElementById: (id) => elements[id] ?? null, + } as unknown as RudderAppDocument, + }; +} + +// codex/019fb444-8493-7973-9439-341f7b35ed2a/019fb444-892e-7462-946a-9ad14220edcc +test('renders the structured launch result from the host', async () => { + const ui = harness(); + await startRudderApp(ui.root, ui.app); + + assert.ok(ui.app.ontoolresult); + ui.app.ontoolresult({ + structuredContent: { + status: 'ready', + title: 'Rudder workspace', + summary: 'Review captured intent.', + fallback: 'Continue from chat.', + }, + }); + + assert.equal(ui.elements.status?.textContent, 'Ready'); + assert.equal(ui.elements.title?.textContent, 'Rudder workspace'); + assert.equal(ui.elements.summary?.textContent, 'Review captured intent.'); + assert.equal(ui.elements.fallback?.textContent, 'Continue from chat.'); +}); + +// codex/019fb444-8493-7973-9439-341f7b35ed2a/019fb444-892e-7462-946a-9ad14220edcc +test('keeps a useful fallback when no launch result or host is available', async () => { + const connected = harness(); + await startRudderApp(connected.root, connected.app); + + assert.ok(connected.app.ontoolresult); + connected.app.ontoolresult({ structuredContent: null }); + assert.equal(connected.elements.status?.textContent, 'Connected'); + + const unavailable = harness(async () => { + throw new Error('No MCP Apps bridge'); + }); + await startRudderApp(unavailable.root, unavailable.app); + assert.equal(unavailable.elements.status?.textContent, 'Unavailable'); + assert.match( + unavailable.elements.fallback?.textContent ?? '', + /needs a host with MCP Apps support/ + ); +}); + +// codex/019fb490-6768-7fe2-917f-a2ae504a4e42/019fb495-1a04-7ff3-845e-cf0eedacb024 +test('shows an error when the host returns malformed content', async () => { + const ui = harness(); + await startRudderApp(ui.root, ui.app); + + assert.ok(ui.app.ontoolresult); + ui.app.ontoolresult({ + structuredContent: { + status: 'ready', + summary: 'Missing its required title.', + fallback: 'Do not render this partial result.', + }, + }); + + assert.equal(ui.elements.status?.textContent, 'Error'); + assert.match( + ui.elements.fallback?.textContent ?? '', + /malformed content from the host/ + ); +}); diff --git a/tsconfig.json b/tsconfig.json index 856d089..b5382e8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,11 +10,12 @@ "strict": true, "skipLibCheck": true, "types": ["node"], - "lib": ["ES2023"] + "lib": ["ES2023", "DOM"] }, "include": [ "bin/**/*.ts", "dangerfile.ts", - "src/**/*.ts" + "src/**/*.ts", + "ui/**/*.ts" ] } diff --git a/ui/rudder-app.html b/ui/rudder-app.html new file mode 100644 index 0000000..ed8823d --- /dev/null +++ b/ui/rudder-app.html @@ -0,0 +1,132 @@ + + + + + + Rudder + + + +
+
+
+ + Rudder +
+ Connecting +
+
+

Rudder

+

+ Turn coding-session intent into focused tests. +

+

+ Use the Rudder skill to generate and verify tests for the current + branch. +

+
+
+ + + diff --git a/ui/rudder-app.ts b/ui/rudder-app.ts new file mode 100644 index 0000000..3c49038 --- /dev/null +++ b/ui/rudder-app.ts @@ -0,0 +1,74 @@ +import { App } from '@modelcontextprotocol/ext-apps'; + +interface LaunchResult { + fallback: string; + status: 'ready'; + summary: string; + title: string; +} + +export type RudderAppBridge = Pick; +export type RudderAppDocument = Pick; + +function element(root: RudderAppDocument, id: string): HTMLElement { + const value = root.getElementById(id); + if (!value) throw new Error(`Missing #${id}`); + return value; +} + +function launchResult(value: unknown): LaunchResult | null { + if (!value || typeof value !== 'object') return null; + const result = value as Record; + if ( + result.status !== 'ready' || + typeof result.title !== 'string' || + typeof result.summary !== 'string' || + typeof result.fallback !== 'string' + ) { + return null; + } + return result as unknown as LaunchResult; +} + +export async function startRudderApp( + root: RudderAppDocument, + app: RudderAppBridge +): Promise { + const status = element(root, 'status'); + const title = element(root, 'title'); + const summary = element(root, 'summary'); + const fallback = element(root, 'fallback'); + + app.ontoolresult = (result) => { + const content = result.structuredContent; + if (content == null) { + status.textContent = 'Connected'; + return; + } + const launch = launchResult(content); + if (!launch) { + status.textContent = 'Error'; + fallback.textContent = 'Rudder received malformed content from the host.'; + return; + } + status.textContent = launch.status === 'ready' ? 'Ready' : launch.status; + title.textContent = launch.title; + summary.textContent = launch.summary; + fallback.textContent = launch.fallback; + }; + + try { + await app.connect(); + } catch { + status.textContent = 'Unavailable'; + fallback.textContent = + 'This view needs a host with MCP Apps support. The Rudder skill remains available from chat.'; + } +} + +if (typeof document !== 'undefined') { + void startRudderApp( + document, + new App({ name: 'Rudder', version: '1.0.0' }) + ); +}