From 65350c096a39126b5ab2e553a8ef17bff3aaed3d Mon Sep 17 00:00:00 2001 From: Ian Date: Thu, 20 Aug 2026 16:01:52 +0100 Subject: [PATCH] Add agentic-context-mcp --- .github/workflows/publish-mcp-server.yml | 92 + README.md | 29 +- core/AGENTS.md | 2 + mcp-server/.gitignore | 2 + mcp-server/README.md | 332 ++++ mcp-server/package-lock.json | 1740 +++++++++++++++++ mcp-server/package.json | 50 + mcp-server/src/cli.ts | 54 + mcp-server/src/content.ts | 251 +++ mcp-server/src/fetchers.ts | 175 ++ mcp-server/src/init.ts | 392 ++++ mcp-server/src/server.ts | 383 ++++ mcp-server/test/cli.test.ts | 204 ++ .../test/content-stale-fallback.test.ts | 28 + mcp-server/test/content.test.ts | 179 ++ mcp-server/test/fetchers.test.ts | 226 +++ .../repo/core/.context/conventions/code.md | 5 + .../.context/conventions/communication.md | 5 + .../core/.context/conventions/workflow.md | 5 + .../test/fixtures/repo/core/.context/index.md | 39 + mcp-server/test/fixtures/repo/core/AGENTS.md | 170 ++ .../repo/playbooks/assess/security.md | 15 + .../repo/playbooks/review/security.md | 11 + .../test/fixtures/repo/standards/security.md | 14 + .../test/fixtures/repo/standards/testing.md | 9 + mcp-server/test/helpers/fixture-server.ts | 47 + mcp-server/test/init.test.ts | 343 ++++ mcp-server/tsconfig.json | 19 + 28 files changed, 4820 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish-mcp-server.yml create mode 100644 mcp-server/.gitignore create mode 100644 mcp-server/README.md create mode 100644 mcp-server/package-lock.json create mode 100644 mcp-server/package.json create mode 100644 mcp-server/src/cli.ts create mode 100644 mcp-server/src/content.ts create mode 100644 mcp-server/src/fetchers.ts create mode 100644 mcp-server/src/init.ts create mode 100644 mcp-server/src/server.ts create mode 100644 mcp-server/test/cli.test.ts create mode 100644 mcp-server/test/content-stale-fallback.test.ts create mode 100644 mcp-server/test/content.test.ts create mode 100644 mcp-server/test/fetchers.test.ts create mode 100644 mcp-server/test/fixtures/repo/core/.context/conventions/code.md create mode 100644 mcp-server/test/fixtures/repo/core/.context/conventions/communication.md create mode 100644 mcp-server/test/fixtures/repo/core/.context/conventions/workflow.md create mode 100644 mcp-server/test/fixtures/repo/core/.context/index.md create mode 100644 mcp-server/test/fixtures/repo/core/AGENTS.md create mode 100644 mcp-server/test/fixtures/repo/playbooks/assess/security.md create mode 100644 mcp-server/test/fixtures/repo/playbooks/review/security.md create mode 100644 mcp-server/test/fixtures/repo/standards/security.md create mode 100644 mcp-server/test/fixtures/repo/standards/testing.md create mode 100644 mcp-server/test/helpers/fixture-server.ts create mode 100644 mcp-server/test/init.test.ts create mode 100644 mcp-server/tsconfig.json diff --git a/.github/workflows/publish-mcp-server.yml b/.github/workflows/publish-mcp-server.yml new file mode 100644 index 0000000..d2c97b4 --- /dev/null +++ b/.github/workflows/publish-mcp-server.yml @@ -0,0 +1,92 @@ +name: Publish MCP Server to npm + +# Publishes the `agentic-context-mcp` package (in mcp-server/) to npm whenever +# a tag matching `mcp-v*` is pushed, e.g. `mcp-v0.2.0`. +# +# Uses npm Trusted Publishing (OIDC) — no NPM_TOKEN secret required. This +# requires one-time setup by whoever owns the `agentic-context-mcp` package +# on npmjs.com, which cannot be done from this workflow file alone: +# +# 1. The package must already exist on npm. If it has never been published, +# the very first publish must be done manually from a maintainer's +# machine (`cd mcp-server && npm publish`), since a Trusted Publisher +# can only be configured for a package that already exists. +# 2. On npmjs.com, go to the package's Settings > Trusted Publisher, choose +# "GitHub Actions", and set: +# - Organization or user: the GitHub org/user owning this repo +# - Repository: this repository's name +# - Workflow filename: publish-mcp-server.yml (filename only) +# - Environment name: leave blank unless using a GitHub Environment +# 3. That's it — no GitHub secret needs to be added. Once configured, any +# push of a `mcp-v*` tag from this workflow is authenticated via a +# short-lived OIDC token, and npm publishes provenance automatically. +# +# See mcp-server/README.md's "Publishing a new version" section for the full +# release process (bumping the version, tagging, etc.). + +on: + push: + tags: + - "mcp-v*" + +permissions: + contents: read + id-token: write # required for npm Trusted Publishing (OIDC) + +jobs: + publish: + runs-on: ubuntu-latest + defaults: + run: + working-directory: mcp-server + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + registry-url: "https://registry.npmjs.org" + + - name: Ensure npm supports Trusted Publishing (>= 11.5.1) + run: npm install -g npm@latest + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Test + run: npm test + + - name: Verify tag matches package.json version + run: | + TAG_VERSION="${GITHUB_REF_NAME#mcp-v}" + PKG_VERSION=$(node -p "require('./package.json').version") + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "Tag mcp-v$TAG_VERSION does not match package.json version $PKG_VERSION" + exit 1 + fi + + - name: Determine npm dist-tag + id: npm-tag + run: | + # npm refuses to guess a dist-tag for prerelease versions (anything + # with a "-" per semver, e.g. 0.1.0-beta.1) — it must be explicit, + # or `npm publish` errors out. Derive it from the prerelease + # identifier (e.g. "beta" from "0.1.0-beta.1"); plain releases get + # "latest". + VERSION=$(node -p "require('./package.json').version") + if [[ "$VERSION" == *-* ]]; then + TAG="${VERSION#*-}" + TAG="${TAG%%.*}" + else + TAG="latest" + fi + echo "Publishing $VERSION with dist-tag: $TAG" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Publish to npm + run: npm publish --provenance --access public --tag "${{ steps.npm-tag.outputs.tag }}" diff --git a/README.md b/README.md index 17f1562..8d5c22e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,25 @@ User says: "refactor the authentication module" `AGENTS.md` is the **single source of truth** for project conventions. All agent config files redirect to it and to `.context/index.md`. -## Quick Start +## MCP Server (Alternative to Deploy) + +Instead of copying files into every repository with `deploy.sh`, let agents query standards and playbooks on demand via the [`agentic-context-mcp`](https://www.npmjs.com/package/agentic-context-mcp) MCP server. No git clone, no build-time bundling, no local install even — it runs via `npx` and fetches markdown over HTTPS at request time (defaulting to `raw.githubusercontent.com` for this repo), caching it in memory for a few hours. + +One command sets everything up — the MCP equivalent of `deploy.sh` — writing an MCP-flavoured `AGENTS.md`, per-agent redirect files, and each agent's MCP server registration: + +```bash +npx agentic-context-mcp init --agents all +``` + +Teams that fork this repo and customise standards/playbooks for their own project can point it at their own published copy instead of upstream, as long as they publish the same directory layout (`core/AGENTS.md`, `core/.context/index.md`, `standards/*.md`, `playbooks/**/*.md`) to any static host (Azure Static Web Apps, Blob Storage static website, GitHub Pages, etc.): + +```bash +npx agentic-context-mcp init --agents all --content-base-url https://your-team-host/agentic-context +``` + +The server exposes 9 tools (`search`, `get_index`, `get_agents_config`, `list_standards`, `get_standard`, `list_playbooks`, `get_playbook`, `list_conventions`, `get_convention`) that let any MCP-compatible agent load exactly the content it needs. See [`mcp-server/README.md`](mcp-server/README.md) for what `init` writes, manual configuration snippets per agent, and instructions for building the server itself from source. + +## Quick Start (Deploy) ```bash ./deploy.sh --agents all /path/to/target-repo @@ -136,6 +154,15 @@ playbooks/ Tier 2 — on demand (→ target .contex discover-local-otel-stack.md use-local-otel-stack.md instrument-dotnet-otel.md + +mcp-server/ MCP server (alternative to deploy.sh) + src/ + cli.ts Bin entry — dispatches to server or init + server.ts MCP server — registers all tools + init.ts `init` command — MCP equivalent of deploy.sh + content.ts Fetches content over HTTPS with in-memory caching + package.json + tsconfig.json ``` > Scripts in `playbooks/setup/create-local-otel-stack/` are deployed to diff --git a/core/AGENTS.md b/core/AGENTS.md index 0d400f5..1e68a7d 100644 --- a/core/AGENTS.md +++ b/core/AGENTS.md @@ -108,6 +108,8 @@ Available context types: - **Playbooks** in `.context/playbooks/` — step-by-step procedures for assessments, reviews, plans, and refactoring - **Conventions** in `.context/conventions/` — workflow, communication, and coding style guidance +> This repository was set up with `deploy.sh`/`deploy.ps1`, which copies these files locally. If you instead configured the `agentic-context-mcp` MCP server (see the [agentic-context repo](https://github.com/ldastey-dev/agentic-context)'s `mcp-server/`), run `npx agentic-context-mcp init` to generate an MCP-flavoured `AGENTS.md` instead of using this one. + --- ## Mandated Standards diff --git a/mcp-server/.gitignore b/mcp-server/.gitignore new file mode 100644 index 0000000..b38db2f --- /dev/null +++ b/mcp-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +build/ diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 0000000..1c0d50c --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,332 @@ +# Agentic Context MCP Server + +An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that serves standards, playbooks, and conventions from the agentic-context library on demand. Connect any AI agent to this server instead of deploying files into every repository. + +## How it works + +There is no git clone and no build-time content bundling. The server fetches markdown files over plain HTTPS at request time and caches them in memory for a few hours: + +- **By default**, it fetches from `raw.githubusercontent.com` for this repo's `main` branch — always current, zero setup. +- **To use your own fork on a static host**, set `CONTENT_BASE_URL` to wherever you publish it (Azure Static Web Apps, Azure Blob Storage static website, GitHub Pages, etc.), as long as the published files mirror this repo's layout (`core/AGENTS.md`, `core/.context/index.md`, `core/.context/conventions/*.md`, `standards/*.md`, `playbooks/**/*.md`). +- **To use a private Azure DevOps repo**, set `CONTENT_SOURCE_TYPE=azure-devops` instead — see [Using a private Azure DevOps repo](#using-a-private-azure-devops-repo) below. No publish step needed; content is read straight from the repo via the Git Items API. +- The list of available standards/playbooks/conventions is derived from `.context/index.md` itself — there's no separate manifest to keep in sync, and no directory-listing API dependency, so this works against any plain static file host or the Azure DevOps API. + +## Quick Start + +No clone, no build, no content directory. Run it directly: + +```bash +npx agentic-context-mcp +``` + +Or install it globally: + +```bash +npm install -g agentic-context-mcp +agentic-context-mcp +``` + +Either way, the process reads MCP requests on stdin/stdout — you'll normally never invoke it manually. Instead, run [`init`](#one-shot-setup-init) below to wire up your agent automatically, or configure it by hand (see [Agent Configuration](#agent-configuration-manual-reference)). + +## One-Shot Setup (`init`) + +`agentic-context-mcp init` is the MCP equivalent of the agentic-context repo's `deploy.sh` — instead of copying ~70 markdown files into your repo, it wires up the MCP server and writes a lean `AGENTS.md`: + +```bash +npx agentic-context-mcp init --agents all +``` + +This writes, for each selected agent: + +- An MCP-flavoured `AGENTS.md` at the repo root (mandated standards + an MCP-specific "Context System" section instructing the agent to use `search`/`get_standard`/`get_playbook`/`get_convention` — **not** the file-based `deploy.sh` instructions). +- Thin per-agent redirect files (`CLAUDE.md`, `.cursor/rules/standards.mdc`, `.windsurfrules`, `.github/copilot-instructions.md`, `.devin/devin.json`) pointing at that `AGENTS.md`. +- The agent's MCP server registration (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.devin/mcp_config.json`) — merged into any existing config rather than overwriting it, so other MCP servers you've already registered are preserved. + +Windsurf's MCP config is global (`~/.codeium/windsurf/mcp_config.json`), not project-scoped, so `init` prints the snippet to add manually instead of writing to your home directory. + +Options: + +```bash +# Target a specific directory instead of the current one +npx agentic-context-mcp init /path/to/repo --agents claude cursor + +# Point at your own published fork instead of upstream +npx agentic-context-mcp init --agents all --content-base-url https://your-team-host/agentic-context + +# Skip files that already exist instead of overwriting them +npx agentic-context-mcp init --agents all --no-overwrite + +# Point at a private Azure DevOps repo instead (PAT via env var, never a CLI arg) +AZURE_DEVOPS_PAT=*** npx agentic-context-mcp init --agents all --source-type azure-devops \ + --azure-org my-org --azure-project my-project --azure-repo agentic-context +``` + +Run `init` only once per repo — after that, editing standards/playbooks upstream (or in your fork) is picked up automatically by the running server, no need to re-run `init`. Re-run it only if you add/remove agents or change the content source. + +## Configuration + +| Env var | Default | Purpose | +| ------- | ------- | ------- | +| `CONTENT_BASE_URL` | `https://raw.githubusercontent.com/ldastey-dev/agentic-context/main` | Base URL content is fetched from (plain static host). Point this at your own published fork. Ignored when `CONTENT_SOURCE_TYPE=azure-devops`. | +| `CONTENT_SOURCE_TYPE` | `raw` | `raw` (fetch `CONTENT_BASE_URL` as a static file host) or `azure-devops` (fetch via the Azure DevOps Git Items API — see below). | +| `CONTENT_AUTH_TOKEN` | _(none)_ | Optional bearer token sent with `raw` requests, for a static host that requires auth. | +| `CACHE_TTL_MINUTES` | `240` (4 hours) | How long fetched files are cached in memory before being re-fetched. | +| `AZURE_DEVOPS_ORG` | _(required for azure-devops)_ | Azure DevOps organisation name (the segment right after `dev.azure.com/`). | +| `AZURE_DEVOPS_PROJECT` | _(required for azure-devops)_ | Azure DevOps project name. | +| `AZURE_DEVOPS_REPO` | _(required for azure-devops)_ | Azure Repos Git repository name. | +| `AZURE_DEVOPS_PAT` | _(required for azure-devops)_ | Personal Access Token with **Code (Read)** scope. Sent as HTTP Basic auth; never logged. | +| `AZURE_DEVOPS_BRANCH` | `main` | Branch/ref to read content from. | +| `AZURE_DEVOPS_API_VERSION` | `7.1` | Azure DevOps REST API version, in case you need to pin an older one. | + +## Using a private Azure DevOps repo + +If your fork of this repo's content lives in a private Azure Repos Git repository (Azure DevOps Services, `dev.azure.com`), point the server at it directly — no static-site publish step required, since the server reads files straight from the repo via the [Git Items REST API](https://learn.microsoft.com/en-us/rest/api/azure/devops/git/items/get?view=azure-devops-rest-7.1). + +1. **Create a PAT** in Azure DevOps (User Settings → Personal Access Tokens) scoped to **Code (Read)** only, for the project containing the repo. +2. **Configure the server** with these env vars instead of `CONTENT_BASE_URL`: + + ```json + { + "mcpServers": { + "agentic-context": { + "command": "npx", + "args": ["-y", "agentic-context-mcp"], + "env": { + "CONTENT_SOURCE_TYPE": "azure-devops", + "AZURE_DEVOPS_ORG": "my-org", + "AZURE_DEVOPS_PROJECT": "my-project", + "AZURE_DEVOPS_REPO": "agentic-context", + "AZURE_DEVOPS_BRANCH": "main", + "AZURE_DEVOPS_PAT": "${file:.devin/credentials/agentic-context}" + } + } + } + } + ``` + + Or use `init` to generate this for you — see [One-Shot Setup](#one-shot-setup-init). `init` never takes the PAT as a CLI argument (that would leak into shell history); it reads `AZURE_DEVOPS_PAT` from the environment to fetch the `AGENTS.md` template during setup, then creates `.devin/credentials/agentic-context` (pre-filled with instructions and added to `.gitignore`) and writes a `${file:...}` reference into the generated MCP config. Replace the contents of `.devin/credentials/agentic-context` with your real PAT afterwards; the server resolves the file reference at startup so the PAT is never stored in the config JSON. +3. **Keep the PAT out of version control.** The `.devin/credentials/` folder is added to `.gitignore` by `init`. Treat the written `.mcp.json` / `.cursor/mcp.json` / etc. the same as any other file containing a secret if you instead embed the PAT directly, or store the PAT in your agent's secret-aware env-var mechanism instead of the JSON file if one is available. +4. **Rotate the PAT** before its expiry (Azure DevOps PATs expire; the server will start failing fetches with a 401/403 once it does — it falls back to serving stale cached content until the cache TTL runs out, then errors). + +This is cloud Azure DevOps Services (`dev.azure.com`) only today; on-premises Azure DevOps Server / TFS uses a different base URL pattern and isn't supported. + +## Publishing your own fork + +If you've customised standards or playbooks for your project, publish the same directory layout as this repo to any static host that serves plain files over HTTPS: + +```text +/ + core/ + AGENTS.md + .context/ + index.md + conventions/{code,workflow,communication}.md + standards/*.md + playbooks/{assess,review,plan,refactor,docs,setup}/*.md +``` + +Then point your server at it: + +```bash +CONTENT_BASE_URL=https://your-team.z13.web.core.windows.net/agentic-context npx -y agentic-context-mcp +``` + +Any CI step that copies these directories to Azure Static Web Apps / Blob Storage `$web` / GitHub Pages on every merge to your fork's default branch keeps the server's content current — no redeploy of the MCP server itself required, since it fetches fresh content up to the cache TTL. + +For a **private** static host, add auth headers in front of it (e.g. an Azure Front Door / APIM rule that injects a key from a private network) — the server does a plain unauthenticated `fetch()`, so any access control needs to happen at the hosting layer, or ask to extend `ContentSource` with a header/token option if you need built-in auth support. + +## Agent Configuration (manual reference) + +`init` (above) writes these for you. Use this section if you'd rather configure an agent by hand, or need to see exactly what `init` produces. + +All examples below use `npx -y agentic-context-mcp` so nothing needs to be installed up front — npm fetches and caches the package on first run. Swap in `agentic-context-mcp` directly (no `npx`) if you installed it globally. + +### Claude Code + +```bash +claude mcp add agentic-context -- npx -y agentic-context-mcp +``` + +With a custom content source: + +```bash +claude mcp add agentic-context -e CONTENT_BASE_URL=https://your-team-host/agentic-context -- npx -y agentic-context-mcp +``` + +Or in `.mcp.json`: + +```json +{ + "mcpServers": { + "agentic-context": { + "command": "npx", + "args": ["-y", "agentic-context-mcp"], + "env": { + "CONTENT_BASE_URL": "https://your-team-host/agentic-context" + } + } + } +} +``` + +### Cursor + +In `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): + +```json +{ + "mcpServers": { + "agentic-context": { + "command": "npx", + "args": ["-y", "agentic-context-mcp"] + } + } +} +``` + +### Windsurf + +In `~/.codeium/windsurf/mcp_config.json`: + +```json +{ + "mcpServers": { + "agentic-context": { + "command": "npx", + "args": ["-y", "agentic-context-mcp"] + } + } +} +``` + +### Devin + +```bash +devin mcp add agentic-context -- npx -y agentic-context-mcp +``` + +Or in `.devin/mcp_config.json`: + +```json +{ + "mcpServers": { + "agentic-context": { + "command": "npx", + "args": ["-y", "agentic-context-mcp"] + } + } +} +``` + +### GitHub Copilot (VS Code) + +In `.vscode/mcp.json`: + +```json +{ + "servers": { + "agentic-context": { + "command": "npx", + "args": ["-y", "agentic-context-mcp"] + } + } +} +``` + +## CLI Commands + +| Command | Purpose | +| ------- | ------- | +| `agentic-context-mcp` | Start the MCP server (stdio). This is what your agent config should invoke. | +| `agentic-context-mcp init [target-dir] [--agents ...]` | One-shot setup: writes `AGENTS.md`, per-agent redirects, and MCP registration into a repo. See [One-Shot Setup](#one-shot-setup-init). | +| `agentic-context-mcp --help` / `-h` | Print usage and the content-source env vars. | +| `agentic-context-mcp init --help` / `-h` | Print `init`'s options, including the Azure DevOps flags. | + +## Available Tools (server) + +| Tool | Description | +| ---- | ----------- | +| `search` | Keyword search across all content — returns matching standards, playbooks, and conventions ranked by relevance | +| `get_index` | Full context index (keyword routing table) | +| `get_agents_config` | AGENTS.md template with mandated standards and `[CONFIGURE]` sections | +| `list_standards` | List all available coding standards | +| `get_standard` | Fetch a specific standard by name (e.g. `security`, `testing`, `dotnet`) | +| `list_playbooks` | List all playbooks, optionally filtered by category | +| `get_playbook` | Fetch a specific playbook by category and name (e.g. `assess` + `security`) | +| `list_conventions` | List all conventions (code, workflow, communication) | +| `get_convention` | Fetch a specific convention by name | + +## Development (working on the server itself) + +The `npx`/global-install instructions above are for *using* the server. If you're changing the server's own code, clone this repo and work from `mcp-server/`: + +```bash +cd mcp-server +npm install + +# Run without a build step +npm run dev + +# Build and test with MCP Inspector +npm run build +npx @modelcontextprotocol/inspector node build/cli.js +``` + +Since content is fetched live from the network (not bundled), editing a standard/playbook in the repo and pushing it is immediately visible to any running server once the cache TTL expires — no rebuild, no republish of this package needed for content changes. Publishing a new version of `agentic-context-mcp` to npm is only required when the server's own code (tools, caching logic, etc.) changes. + +### Tests + +```bash +npm test +``` + +Runs on Node's built-in test runner (`node:test`, via `tsx`) — no extra test framework dependency. `pretest` builds the project first, since the CLI tests spawn the compiled binary (`build/cli.js`) as a real child process, mirroring how the agentic-context repo's own `tests/test-deploy.sh` invokes `deploy.sh` as a black box and asserts on its output. + +| File | Covers | +| ---- | ------ | +| `test/content.test.ts` | `ContentSource` — fetching, manifest parsing from `index.md`, search ranking, in-memory caching | +| `test/content-stale-fallback.test.ts` | Serving stale cached content instead of throwing when a refetch fails | +| `test/fetchers.test.ts` | `RawUrlFetcher` / `AzureDevOpsFetcher` / `createFetcher` — URL construction, Basic/Bearer auth headers, env var fallback, missing-config errors | +| `test/init.test.ts` | `init` — AGENTS.md transformation, per-agent redirects, MCP config JSON merge (including preserving unrelated pre-existing servers), `--no-overwrite`, Windsurf's print-only path, the Azure DevOps source flow (PAT never leaks into the written config) | +| `test/cli.test.ts` | The built binary end-to-end: MCP handshake + `tools/list` + `tools/call` over stdio, the `init` subcommand writing real files, and `--help`/`init --help` | + +Most tests run against a local fixture HTTP server (`test/helpers/fixture-server.ts`) serving `test/fixtures/repo/` — a minimal stand-in for this repo's layout — so the suite is fast, deterministic, and doesn't depend on GitHub being reachable. The exceptions: one test ("omits the env override when `--content-base-url` is not provided") deliberately exercises the real upstream default, and the Azure DevOps tests (`test/fetchers.test.ts`, and the `azure-devops source: ...` tests in `test/init.test.ts`) stub `global.fetch` directly instead, since there's no local Azure DevOps server to stand in for `dev.azure.com`. + +Run just the `content`/`init` unit tests (skipping the slower CLI process-spawning tests) during development: + +```bash +npm run test:unit +``` + +### Publishing a new version + +Releases are published by [`.github/workflows/publish-mcp-server.yml`](../.github/workflows/publish-mcp-server.yml) whenever a tag matching `mcp-v*` is pushed, using [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC) — no `NPM_TOKEN` secret involved. + +```bash +cd mcp-server +npm version --no-git-tag-version # bumps package.json + package-lock.json only +git add package.json package-lock.json +git commit -m "chore(mcp-server): bump version to $(node -p "require('./package.json').version")" +git tag "mcp-v$(node -p "require('./package.json').version")" +git push && git push --tags +``` + +`npm version` normally creates a `vX.Y.Z` tag by default, which won't match the workflow's trigger — hence `--no-git-tag-version` plus tagging manually with the `mcp-v` prefix. Pushing that tag triggers the workflow, which builds, tests, and publishes. + +**Prerelease versions (`0.1.0-beta.1`, `1.0.0-rc.1`, etc.) need a dist-tag.** npm refuses to guess one for any version containing a `-`, since it won't silently make a prerelease the `latest` install target — `npm publish` errors with `You must specify a tag using --tag when publishing a prerelease version.` if you don't pass one. The workflow handles this automatically (derives `beta`/`rc`/etc. from the version string, falls back to `latest` for plain releases), but if you ever publish manually, pass it yourself: + +```bash +npm publish --tag beta # or whatever the prerelease identifier is +``` + +Forgetting `--tag` on a prerelease is also caught by `npm publish --dry-run`, which is worth running locally before pushing a release tag. + +**One-time setup required before this works** (only the npm package owner can do this): + +1. **First publish must be manual.** A Trusted Publisher can only be configured for a package that already exists on npm, so the very first release has to be `npm publish` run locally by someone with publish rights. If `package.json`'s version is a prerelease at that point, remember `--tag` (above). +2. **Configure the Trusted Publisher** on the package's npmjs.com Settings page → "Trusted Publisher" → GitHub Actions, with: + - Organization/user and repository set to this GitHub repo + - Workflow filename: `publish-mcp-server.yml` + - Environment name: left blank (not used here) + +After that, all subsequent releases go through the tag-triggered workflow. Contributors without npm/repo-admin access can still open PRs that change the workflow itself — GitHub withholds secrets and OIDC tokens from fork-originated pull request runs, so the workflow only actually publishes once merged and run from the base repo. diff --git a/mcp-server/package-lock.json b/mcp-server/package-lock.json new file mode 100644 index 0000000..5495de9 --- /dev/null +++ b/mcp-server/package-lock.json @@ -0,0 +1,1740 @@ +{ + "name": "agentic-context-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agentic-context-mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "zod": "^3.24.0" + }, + "bin": { + "agentic-context-mcp": "build/index.js" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "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==", + "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/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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==", + "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.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "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==", + "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.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "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==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "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/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.3.tgz", + "integrity": "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "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==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", + "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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==", + "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==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "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", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "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==", + "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==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "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==", + "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==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "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.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "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==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp-server/package.json b/mcp-server/package.json new file mode 100644 index 0000000..75e2e2a --- /dev/null +++ b/mcp-server/package.json @@ -0,0 +1,50 @@ +{ + "name": "agentic-context-mcp", + "version": "0.1.1-beta.1", + "description": "MCP server serving agentic-context standards, playbooks, and conventions on demand, fetched over HTTPS with in-memory caching", + "type": "module", + "bin": { + "agentic-context-mcp": "build/cli.js" + }, + "files": [ + "build" + ], + "scripts": { + "build": "tsc", + "start": "node build/cli.js", + "dev": "tsx src/cli.ts", + "pretest": "npm run build", + "test": "tsx --test test/**/*.test.ts", + "test:unit": "tsx --test test/content.test.ts test/content-stale-fallback.test.ts test/init.test.ts" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "agentic-context", + "standards", + "playbooks", + "coding-standards" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ldastey-dev/agentic-context.git", + "directory": "mcp-server" + }, + "homepage": "https://github.com/ldastey-dev/agentic-context/tree/main/mcp-server#readme", + "bugs": { + "url": "https://github.com/ldastey-dev/agentic-context/issues" + }, + "engines": { + "node": ">=20.0.0" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/mcp-server/src/cli.ts b/mcp-server/src/cli.ts new file mode 100644 index 0000000..9be3dae --- /dev/null +++ b/mcp-server/src/cli.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +/** + * CLI entry point for `agentic-context-mcp`. + * + * With no subcommand, starts the MCP server on stdio (the normal path when + * an agent launches this as `npx -y agentic-context-mcp`). + * + * `agentic-context-mcp init [target-dir] [--agents ...]` runs the one-shot + * setup equivalent of the agentic-context repo's `deploy.sh`, but configured + * for MCP usage instead of copying files. + * + * `agentic-context-mcp --help` / `agentic-context-mcp init --help` print + * usage without starting the server or touching the network. + */ + +const HELP = `agentic-context-mcp — MCP server for agentic-context standards, playbooks, and conventions + +Usage: + agentic-context-mcp Start the MCP server on stdio (this is what your agent config should invoke) + agentic-context-mcp init [options] One-shot setup: writes AGENTS.md, per-agent redirects, and MCP registration into a repo + agentic-context-mcp --help Show this help + agentic-context-mcp init --help Show init's options + +Content source (env vars, read by the server): + CONTENT_BASE_URL Base URL for a static host (default: raw.githubusercontent.com/ldastey-dev/agentic-context/main) + CONTENT_SOURCE_TYPE "raw" (default) or "azure-devops" + CONTENT_AUTH_TOKEN Optional bearer token for a private "raw" static host + CACHE_TTL_MINUTES Minutes to cache fetched content (default: 240) + AZURE_DEVOPS_ORG/PROJECT/REPO/PAT/BRANCH/API_VERSION Required when CONTENT_SOURCE_TYPE=azure-devops + +See mcp-server/README.md in the agentic-context repo for full documentation. +`; + +const [, , command, ...rest] = process.argv; + +async function main() { + if (command === "--help" || command === "-h") { + console.log(HELP); + return; + } + if (command === "init") { + const { runInit } = await import("./init.js"); + await runInit(rest); + } else { + const { startServer } = await import("./server.js"); + await startServer(); + } +} + +main().catch((err) => { + console.error("[agentic-context-mcp] Fatal error:", err); + process.exit(1); +}); diff --git a/mcp-server/src/content.ts b/mcp-server/src/content.ts new file mode 100644 index 0000000..1eafcf5 --- /dev/null +++ b/mcp-server/src/content.ts @@ -0,0 +1,251 @@ +/** + * Content source — fetches standards, playbooks, and conventions over plain + * HTTPS from wherever they're published, with an in-memory TTL cache. + * + * No git clone, no build-time bundling, no local checkout required. + * + * Resolution (see fetchers.ts for the full precedence rules): + * - CONTENT_BASE_URL env var, if set, fetches from a plain static host. + * Point this at a team's own published fork (Azure Static Web Apps, + * Blob Storage static site, GitHub Pages, etc.) as long as it mirrors + * this repo's file layout. + * - CONTENT_SOURCE_TYPE=azure-devops switches to the Azure DevOps Git + * Items REST API instead, for teams whose fork lives in a private Azure + * Repos repo. Requires AZURE_DEVOPS_ORG, AZURE_DEVOPS_PROJECT, + * AZURE_DEVOPS_REPO, and AZURE_DEVOPS_PAT. + * - Otherwise defaults to raw.githubusercontent.com for this repo's main + * branch. + * + * The manifest of available standards/playbooks/conventions is derived from + * `.context/index.md` itself (the keyword routing table already lists every + * file with its keywords and summary) — no separate manifest file needed, + * and no directory-listing API dependency, so this works against any plain + * static file host. + */ + +import { createFetcher, type AzureDevOpsFetcherOptions, type SourceFetcher } from "./fetchers.js"; + +const DEFAULT_CACHE_TTL_MINUTES = 240; // 4 hours + +interface CacheEntry { + text: string; + fetchedAt: number; +} + +export interface ManifestEntry { + keywords: string; + sourcePath: string; + summary: string; + category: "standard" | "playbook" | "convention" | "unknown"; + subcategory?: string; + name: string; +} + +export class ContentSource { + private fetcher: SourceFetcher; + private configError?: Error; + private cacheTtlMs: number; + private cache = new Map(); + private manifestCache: ManifestEntry[] | null = null; + private manifestFetchedAt = 0; + + constructor(options?: { + baseUrl?: string; + cacheTtlMinutes?: number; + fetcher?: SourceFetcher; + sourceType?: string; + authToken?: string; + azureDevOps?: Partial; + }) { + if (options?.fetcher) { + this.fetcher = options.fetcher; + } else { + try { + this.fetcher = createFetcher({ + baseUrl: options?.baseUrl, + sourceType: options?.sourceType, + authToken: options?.authToken, + azureDevOps: options?.azureDevOps, + }); + } catch (err) { + this.configError = err as Error; + this.fetcher = { + describe: () => `unconfigured: ${this.configError!.message}`, + fetch: async () => { + throw this.configError!; + }, + }; + } + } + + const ttlMinutes = + options?.cacheTtlMinutes ?? + Number(process.env.CACHE_TTL_MINUTES) ?? + DEFAULT_CACHE_TTL_MINUTES; + this.cacheTtlMs = (Number.isFinite(ttlMinutes) && ttlMinutes > 0 + ? ttlMinutes + : DEFAULT_CACHE_TTL_MINUTES) * 60 * 1000; + } + + /** Human-readable description of the active content source, for logs. */ + describeSource(): string { + return this.fetcher.describe(); + } + + /** Fetch a file at a repo-relative path, using the cache when fresh. */ + private async fetchFile(relativePath: string, forceFresh = false): Promise { + const cached = this.cache.get(relativePath); + const isFresh = cached && Date.now() - cached.fetchedAt < this.cacheTtlMs; + + if (!forceFresh && isFresh) { + return cached.text; + } + + if (this.configError) throw this.configError; + + const description = `${this.fetcher.describe()}/${relativePath}`; + let response: Response; + try { + response = await this.fetcher.fetch(relativePath); + } catch (err) { + if (cached) return cached.text; // serve stale on network failure + throw new Error(`Failed to fetch ${description}: ${err}`); + } + + if (!response.ok) { + if (cached) return cached.text; // serve stale rather than fail hard + throw new Error(`${response.status} ${response.statusText} fetching ${description}`); + } + + const text = await response.text(); + this.cache.set(relativePath, { text, fetchedAt: Date.now() }); + return text; + } + + async getIndex(): Promise { + return this.fetchFile("core/.context/index.md"); + } + + async getAgentsConfig(): Promise { + return this.fetchFile("core/AGENTS.md"); + } + + async getStandard(name: string): Promise { + return this.fetchFile(`standards/${name}.md`); + } + + async getPlaybook(category: string, name: string): Promise { + return this.fetchFile(`playbooks/${category}/${name}.md`); + } + + async getConvention(name: string): Promise { + return this.fetchFile(`core/.context/conventions/${name}.md`); + } + + /** Parse `.context/index.md` into a structured manifest of every entry. */ + async getManifest(): Promise { + const isFresh = + this.manifestCache && Date.now() - this.manifestFetchedAt < this.cacheTtlMs; + if (isFresh) return this.manifestCache!; + + const index = await this.getIndex(); + const entries: ManifestEntry[] = []; + + for (const line of index.split("\n")) { + const match = line.match(/^\|\s*(.+?)\s*\|\s*`(.+?)`\s*\|\s*(.+?)\s*\|$/); + if (!match) continue; + const [, keywords, targetPath, summary] = match; + if (targetPath.startsWith("Keywords")) continue; // header row guard + + const entry = this.mapTargetPathToEntry(targetPath, keywords, summary); + if (entry) entries.push(entry); + } + + this.manifestCache = entries; + this.manifestFetchedAt = Date.now(); + return entries; + } + + /** Translate a target-repo path from index.md (e.g. `.context/standards/x.md`) + * into the source-repo-relative path and category/name metadata. */ + private mapTargetPathToEntry( + targetPath: string, + keywords: string, + summary: string + ): ManifestEntry | null { + let category: ManifestEntry["category"] = "unknown"; + let sourcePath = ""; + let subcategory: string | undefined; + let name = ""; + + if (targetPath.startsWith(".context/standards/")) { + category = "standard"; + const file = targetPath.slice(".context/standards/".length); + sourcePath = `standards/${file}`; + name = file.replace(/\.md$/, ""); + } else if (targetPath.startsWith(".context/playbooks/")) { + category = "playbook"; + const rest = targetPath.slice(".context/playbooks/".length); + const parts = rest.split("/"); + subcategory = parts[0]; + const file = parts.slice(1).join("/"); + sourcePath = `playbooks/${rest}`; + name = file.replace(/\.md$/, ""); + } else if (targetPath.startsWith(".context/conventions/")) { + category = "convention"; + const file = targetPath.slice(".context/conventions/".length); + sourcePath = `core/.context/conventions/${file}`; + name = file.replace(/\.md$/, ""); + } else { + return null; + } + + return { keywords, sourcePath, summary, category, subcategory, name }; + } + + async listStandards(): Promise { + const manifest = await this.getManifest(); + return manifest.filter((e) => e.category === "standard"); + } + + async listPlaybooks(category?: string): Promise { + const manifest = await this.getManifest(); + return manifest.filter( + (e) => e.category === "playbook" && (!category || e.subcategory === category) + ); + } + + async listConventions(): Promise { + const manifest = await this.getManifest(); + return manifest.filter((e) => e.category === "convention"); + } + + /** Keyword search across the manifest, ranked by how many terms match. */ + async search( + query: string + ): Promise> { + const manifest = await this.getManifest(); + const terms = query.toLowerCase().split(/\s+/).filter(Boolean); + + const results: Array = []; + + for (const entry of manifest) { + const keywordList = entry.keywords.toLowerCase().split(",").map((k) => k.trim()); + const matchedTerms = terms.filter((term) => keywordList.some((k) => k.includes(term))); + if (matchedTerms.length === 0) continue; + + const relevance = + matchedTerms.length === terms.length + ? "high" + : matchedTerms.length > 1 + ? "medium" + : "low"; + + results.push({ ...entry, relevance }); + } + + const order = { high: 0, medium: 1, low: 2 }; + results.sort((a, b) => order[a.relevance] - order[b.relevance]); + return results; + } +} diff --git a/mcp-server/src/fetchers.ts b/mcp-server/src/fetchers.ts new file mode 100644 index 0000000..4893da4 --- /dev/null +++ b/mcp-server/src/fetchers.ts @@ -0,0 +1,175 @@ +/** + * Source fetchers — pluggable strategies for retrieving a single repo-relative + * file's raw contents over HTTPS. `ContentSource` (content.ts) owns caching, + * manifest parsing, and search; it doesn't know or care which fetcher is + * behind it. + * + * Two implementations ship today: + * - `RawUrlFetcher` — concatenates a base URL with the relative path. + * Works for raw.githubusercontent.com, GitHub Pages, Azure Static Web + * Apps, Blob Storage static sites, or any plain static file host. + * Optionally sends a bearer token for hosts that require auth. + * - `AzureDevOpsFetcher` — calls the Azure DevOps Git Items REST API + * (`_apis/git/repositories/{repo}/items?path=...`), which is required + * for private Azure Repos since there is no static raw-file endpoint. + * Authenticates with a PAT via HTTP Basic auth. + */ + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +export interface SourceFetcher { + /** Human-readable description of where content comes from, for logs. */ + describe(): string; + /** Fetch a repo-relative file (e.g. "standards/security.md"). */ + fetch(relativePath: string): Promise; +} + +export const AZURE_DEVOPS_PAT_INSTRUCTIONS = `Paste your Azure DevOps Personal Access Token (PAT) into this file, replacing these instructions. It needs only Code (Read) scope. This file is gitignored — do not commit it.`; + +const DEFAULT_BASE_URL = + "https://raw.githubusercontent.com/ldastey-dev/agentic-context/main"; + +export class RawUrlFetcher implements SourceFetcher { + constructor( + private readonly baseUrl: string, + private readonly authToken?: string + ) {} + + describe(): string { + return this.baseUrl; + } + + fetch(relativePath: string): Promise { + const url = `${this.baseUrl}/${relativePath}`; + const headers: Record = {}; + if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`; + return fetch(url, { headers }); + } +} + +export interface AzureDevOpsFetcherOptions { + organization: string; + project: string; + repository: string; + /** Branch/ref to read from. Defaults to "main". */ + branch?: string; + /** Personal Access Token with at least Code (Read) scope. */ + pat: string; + /** Azure DevOps REST API version. Defaults to "7.1". */ + apiVersion?: string; +} + +export class AzureDevOpsFetcher implements SourceFetcher { + private readonly branch: string; + private readonly apiVersion: string; + + constructor(private readonly opts: AzureDevOpsFetcherOptions) { + this.branch = opts.branch || "main"; + this.apiVersion = opts.apiVersion || "7.1"; + } + + describe(): string { + return `azure-devops:${this.opts.organization}/${this.opts.project}/${this.opts.repository}@${this.branch}`; + } + + fetch(relativePath: string): Promise { + const { organization, project, repository, pat } = this.opts; + const path = relativePath.startsWith("/") ? relativePath : `/${relativePath}`; + + const url = + `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}` + + `/_apis/git/repositories/${encodeURIComponent(repository)}/items` + + `?path=${encodeURIComponent(path)}` + + `&versionDescriptor.version=${encodeURIComponent(this.branch)}` + + `&api-version=${this.apiVersion}&$format=text`; + + // Azure DevOps PATs authenticate over Basic auth with an empty username. + const token = Buffer.from(`:${pat}`).toString("base64"); + return fetch(url, { headers: { Authorization: `Basic ${token}` } }); + } +} + +export interface FetcherOptions { + /** Plain static-host base URL. If set, always builds a RawUrlFetcher. */ + baseUrl?: string; + /** Bearer token to send with RawUrlFetcher requests, if the host needs auth. */ + authToken?: string; + /** "raw" (default) or "azure-devops". Ignored when `baseUrl` is set. */ + sourceType?: string; + azureDevOps?: Partial; +} + +function resolveEnvReference(value?: string): string | undefined { + if (!value) return value; + const match = value.match(/^\$\{file:(.+)\}$/); + if (!match) return value; + const filePath = match[1]; + try { + return readFileSync(resolve(filePath), "utf-8").replace(/\r?\n+$/, ""); + } catch (err) { + throw new Error(`Failed to read file referenced by ${value}: ${filePath} — ${err}`); + } +} + +/** + * Build a fetcher from explicit options, falling back to environment + * variables for anything not passed in — mirroring the precedence already + * used for `CONTENT_BASE_URL` (explicit option > env var > default). + */ +export function createFetcher(options?: FetcherOptions): SourceFetcher { + if (options?.baseUrl) { + return new RawUrlFetcher( + options.baseUrl.replace(/\/+$/, ""), + resolveEnvReference(options.authToken ?? process.env.CONTENT_AUTH_TOKEN) + ); + } + + const sourceType = (options?.sourceType ?? process.env.CONTENT_SOURCE_TYPE ?? "raw").toLowerCase(); + + if (sourceType === "azure-devops") { + const organization = options?.azureDevOps?.organization ?? process.env.AZURE_DEVOPS_ORG; + const project = options?.azureDevOps?.project ?? process.env.AZURE_DEVOPS_PROJECT; + const repository = options?.azureDevOps?.repository ?? process.env.AZURE_DEVOPS_REPO; + const branch = options?.azureDevOps?.branch ?? process.env.AZURE_DEVOPS_BRANCH; + const rawPat = options?.azureDevOps?.pat ?? process.env.AZURE_DEVOPS_PAT; + const pat = resolveEnvReference(rawPat); + const apiVersion = options?.azureDevOps?.apiVersion ?? process.env.AZURE_DEVOPS_API_VERSION; + + if (pat && pat.includes(AZURE_DEVOPS_PAT_INSTRUCTIONS)) { + throw new Error( + "The Azure DevOps PAT is still the placeholder instructions. Replace the contents of .devin/credentials/agentic-context with your real PAT (Code (Read) scope)." + ); + } + + const missing = [ + ["AZURE_DEVOPS_ORG", organization], + ["AZURE_DEVOPS_PROJECT", project], + ["AZURE_DEVOPS_REPO", repository], + ["AZURE_DEVOPS_PAT", pat], + ] + .filter(([, value]) => !value) + .map(([name]) => name); + + if (missing.length > 0) { + throw new Error( + `CONTENT_SOURCE_TYPE=azure-devops requires ${missing.join(", ")} to be set.` + ); + } + + return new AzureDevOpsFetcher({ + organization: organization!, + project: project!, + repository: repository!, + branch, + pat: pat!, + apiVersion, + }); + } + + const baseUrl = process.env.CONTENT_BASE_URL ?? DEFAULT_BASE_URL; + return new RawUrlFetcher( + baseUrl.replace(/\/+$/, ""), + resolveEnvReference(options?.authToken ?? process.env.CONTENT_AUTH_TOKEN) + ); +} diff --git a/mcp-server/src/init.ts b/mcp-server/src/init.ts new file mode 100644 index 0000000..0913ab0 --- /dev/null +++ b/mcp-server/src/init.ts @@ -0,0 +1,392 @@ +/** + * `agentic-context-mcp init` — the MCP equivalent of the agentic-context + * repo's `deploy.sh`. Instead of copying ~70 markdown files into the target + * repo, it: + * + * 1. Writes an MCP-flavoured `AGENTS.md` (derived from the canonical + * `core/AGENTS.md` template, with the file-based "Context System" + * section swapped for MCP tool-call instructions). + * 2. Writes thin per-agent redirect files (CLAUDE.md, .cursor/rules/, + * .windsurfrules, .github/copilot-instructions.md, .devin/devin.json) + * pointing at that AGENTS.md. + * 3. Registers the `agentic-context-mcp` server in each selected agent's + * MCP config file (merging into any existing config rather than + * clobbering it). + * + * Usage: + * npx agentic-context-mcp init [target-dir] [--agents claude cursor ...] + * [--content-base-url URL] [--overwrite|--no-overwrite] + * + * # Point the generated MCP config at a private Azure DevOps repo instead + * # of a plain static host. The PAT itself is never taken as a CLI arg + * # (avoids shell-history leakage) — set AZURE_DEVOPS_PAT so `init` can + * # fetch the AGENTS.md template from it, then fill in the placeholder + * # left in the written MCP config yourself. + * AZURE_DEVOPS_PAT=*** npx agentic-context-mcp init --source-type azure-devops \ + * --azure-org my-org --azure-project my-project --azure-repo agentic-context + */ + +import { existsSync } from "node:fs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { ContentSource } from "./content.js"; +import { AZURE_DEVOPS_PAT_INSTRUCTIONS } from "./fetchers.js"; + +const VALID_AGENTS = ["claude", "copilot", "cursor", "devin", "windsurf"] as const; +type Agent = (typeof VALID_AGENTS)[number]; + +interface InitOptions { + targetDir: string; + agents: Agent[]; + overwrite: boolean | null; // null = warn-and-overwrite (no interactive prompt in a one-shot CLI) + contentBaseUrl?: string; + sourceType?: string; + azureOrg?: string; + azureProject?: string; + azureRepo?: string; + azureBranch?: string; +} + +const MCP_CONTEXT_SYSTEM_SECTION = `## Context System + +This project uses the \`agentic-context\` MCP server for on-demand access to standards, playbooks, and conventions. No files are deployed into this repository — you MUST use the MCP tools rather than assuming file paths exist or relying on prior knowledge of standards. + +Before starting any non-trivial task: + +1. Call the \`search\` tool with keywords describing the task (e.g. "security OWASP", "refactor code smell") to find relevant standards and playbooks. +2. Call \`get_standard\`, \`get_playbook\`, or \`get_convention\` to load the full content before proceeding. +3. If unsure what's available, call \`get_index\` for the full keyword routing table, or \`list_standards\` / \`list_playbooks\` / \`list_conventions\` to browse everything. + +If the \`agentic-context\` MCP tools are not available in this session, stop and tell the user before proceeding — do not silently fall back to unaided judgement on standards that should be loaded from the MCP server. + +--- +`; + +const MCP_STANDARDS_INTRO = + "The following standards are non-negotiable. Do not weaken them. Detailed guidance is available via the `get_standard` tool — strip `.context/standards/` and `.md` from the path below to get the standard name (e.g. `.context/standards/code-quality.md` → `get_standard(\"code-quality\")`)."; + +const REDIRECT_NOTE = + 'This project uses the `agentic-context` MCP server for on-demand standards and playbooks. Call its tools (`search`, `get_standard`, `get_playbook`, `get_convention`) before starting any task — see `AGENTS.md` for details.'; + +export const INIT_HELP = `agentic-context-mcp init [target-dir] [options] + +One-shot setup for a target repo — the MCP equivalent of the agentic-context +repo's deploy.sh. Writes an MCP-flavoured AGENTS.md, thin per-agent redirect +files, and the agentic-context MCP server registration for each selected +agent. Run once per repo; re-run only if you add/remove agents or change the +content source. + +Arguments: + target-dir Directory to write into (default: current directory) + +Options: + --agents Agents to configure: ${VALID_AGENTS.join(" ")} (default: all) + --overwrite Overwrite existing files without prompting + --no-overwrite Skip files that already exist instead of overwriting them + --content-base-url Fetch content from a static host instead of upstream GitHub + --source-type Content source: "raw" (default) or "azure-devops" + --azure-org Azure DevOps organisation (azure-devops source only) + --azure-project Azure DevOps project (azure-devops source only) + --azure-repo Azure DevOps repository (azure-devops source only) + --azure-branch Azure DevOps branch (default: main; azure-devops source only) + -h, --help Show this help and exit + +For the azure-devops source, the PAT is never passed as a CLI flag (it would +leak into shell history) — set the AZURE_DEVOPS_PAT environment variable +during init so it can fetch AGENTS.md. The generated .devin/credentials/agentic-context +file is pre-filled with instructions; replace its contents with your PAT +(Code (Read) scope). The credentials folder is added to .gitignore. + +Examples: + npx agentic-context-mcp init --agents all + npx agentic-context-mcp init /path/to/repo --agents claude cursor + npx agentic-context-mcp init --agents all --content-base-url https://your-team-host/agentic-context + AZURE_DEVOPS_PAT=*** npx agentic-context-mcp init --source-type azure-devops \\ + --azure-org my-org --azure-project my-project --azure-repo agentic-context + +See mcp-server/README.md in the agentic-context repo for full documentation. +`; + +function parseArgs(argv: string[]): InitOptions { + let targetDir = process.cwd(); + let agents: Agent[] = []; + let overwrite: boolean | null = null; + let contentBaseUrl = process.env.CONTENT_BASE_URL; + let sourceType = process.env.CONTENT_SOURCE_TYPE; + let azureOrg = process.env.AZURE_DEVOPS_ORG; + let azureProject = process.env.AZURE_DEVOPS_PROJECT; + let azureRepo = process.env.AZURE_DEVOPS_REPO; + let azureBranch = process.env.AZURE_DEVOPS_BRANCH; + + const args = [...argv]; + while (args.length > 0) { + const arg = args.shift()!; + if (arg === "--agents") { + while (args.length > 0 && !args[0].startsWith("--")) { + const value = args.shift()!; + if (value === "all") { + agents.push(...VALID_AGENTS); + } else if ((VALID_AGENTS as readonly string[]).includes(value)) { + agents.push(value as Agent); + } else { + console.error(`[init] Unknown agent "${value}" — skipping. Valid: ${VALID_AGENTS.join(", ")}, all`); + } + } + } else if (arg === "--overwrite") { + overwrite = true; + } else if (arg === "--no-overwrite") { + overwrite = false; + } else if (arg === "--content-base-url") { + contentBaseUrl = args.shift(); + } else if (arg === "--source-type") { + sourceType = args.shift(); + } else if (arg === "--azure-org") { + azureOrg = args.shift(); + } else if (arg === "--azure-project") { + azureProject = args.shift(); + } else if (arg === "--azure-repo") { + azureRepo = args.shift(); + } else if (arg === "--azure-branch") { + azureBranch = args.shift(); + } else if (!arg.startsWith("--")) { + targetDir = resolve(arg); + } + } + + if (agents.length === 0) { + agents = [...VALID_AGENTS]; + } + agents = [...new Set(agents)]; + + return { targetDir, agents, overwrite, contentBaseUrl, sourceType, azureOrg, azureProject, azureRepo, azureBranch }; +} + +async function writeFileGuarded(filePath: string, content: string, overwrite: boolean | null): Promise { + const exists = existsSync(filePath); + if (exists && overwrite === false) { + console.error(`[init] Skipped (already exists): ${filePath}`); + return; + } + if (exists && overwrite === null) { + console.error(`[init] Overwriting existing file (use --no-overwrite to skip instead): ${filePath}`); + } + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, content, "utf-8"); + console.error(`[init] Wrote ${filePath}`); +} + +/** Merge an MCP server entry into an existing JSON config file rather than clobbering it. */ +async function mergeMcpConfig( + filePath: string, + rootKey: "mcpServers" | "servers", + serverName: string, + serverConfig: Record +): Promise { + let existing: Record = {}; + if (existsSync(filePath)) { + try { + existing = JSON.parse(await readFile(filePath, "utf-8")); + } catch { + console.error(`[init] Warning: ${filePath} exists but is not valid JSON — it will be overwritten.`); + existing = {}; + } + } + + const servers = (existing[rootKey] as Record) || {}; + servers[serverName] = serverConfig; + existing[rootKey] = servers; + + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8"); + console.error(`[init] Updated ${filePath} (${rootKey}.${serverName})`); +} + +async function writeAzureCredentialsFiles(options: InitOptions): Promise { + if (options.sourceType !== "azure-devops") return; + + const credentialsDir = join(options.targetDir, ".devin", "credentials"); + const credentialsFile = join(credentialsDir, "agentic-context"); + if (!existsSync(credentialsFile)) { + await mkdir(credentialsDir, { recursive: true }); + await writeFile(credentialsFile, AZURE_DEVOPS_PAT_INSTRUCTIONS + "\n", "utf-8"); + console.error(`[init] Wrote ${credentialsFile}`); + } else { + console.error(`[init] Skipped (already exists): ${credentialsFile}`); + } + + await ensureGitignoreEntry(options.targetDir, ".devin/credentials/"); +} + +async function ensureGitignoreEntry(targetDir: string, entry: string): Promise { + const gitignorePath = join(targetDir, ".gitignore"); + let content = ""; + if (existsSync(gitignorePath)) { + content = await readFile(gitignorePath, "utf-8"); + if (content.includes(entry)) { + console.error(`[init] .gitignore already ignores ${entry}`); + return; + } + if (!content.endsWith("\n")) content += "\n"; + } + content += entry + "\n"; + await writeFile(gitignorePath, content, "utf-8"); + console.error(`[init] Updated .gitignore to ignore ${entry}`); +} + +const AZURE_DEVOPS_PAT_FILE_REF = "${file:.devin/credentials/agentic-context}"; + +function buildServerConfig(options: InitOptions): Record { + const config: Record = { + command: "npx", + args: ["-y", "agentic-context-mcp"], + }; + + if (options.sourceType === "azure-devops") { + const env: Record = { CONTENT_SOURCE_TYPE: "azure-devops" }; + if (options.azureOrg) env.AZURE_DEVOPS_ORG = options.azureOrg; + if (options.azureProject) env.AZURE_DEVOPS_PROJECT = options.azureProject; + if (options.azureRepo) env.AZURE_DEVOPS_REPO = options.azureRepo; + if (options.azureBranch) env.AZURE_DEVOPS_BRANCH = options.azureBranch; + env.AZURE_DEVOPS_PAT = AZURE_DEVOPS_PAT_FILE_REF; + config.env = env; + } else if (options.contentBaseUrl) { + config.env = { CONTENT_BASE_URL: options.contentBaseUrl }; + } + + return config; +} + +/** Fetch the canonical AGENTS.md template and swap the file-based Context + * System section for MCP tool-call instructions. */ +function transformAgentsMd(agentsMd: string): string { + let result = agentsMd.replace( + /## Context System[\s\S]*?\n---\n/, + MCP_CONTEXT_SYSTEM_SECTION + ); + result = result.replace( + /The following standards are non-negotiable\. Do not weaken them\. Detailed guidance is in `\.context\/standards\/`\./, + MCP_STANDARDS_INTRO + ); + return result; +} + +async function writeAgentsMd(source: ContentSource, options: InitOptions): Promise { + const template = await source.getAgentsConfig(); + const transformed = transformAgentsMd(template); + await writeFileGuarded(join(options.targetDir, "AGENTS.md"), transformed, options.overwrite); +} + +async function writeAgentRedirects(options: InitOptions): Promise { + const { targetDir, overwrite } = options; + + if (options.agents.includes("claude")) { + const content = `# CLAUDE.md\n\nRead and apply \`AGENTS.md\` for project conventions and workflow rules.\n\n## Context System\n\n${REDIRECT_NOTE}\n`; + await writeFileGuarded(join(targetDir, "CLAUDE.md"), content, overwrite); + } + + if (options.agents.includes("cursor")) { + const content = `Follow the rules defined in AGENTS.md at the repository root. That file is the\nsingle source of truth for all coding standards, architecture decisions, and\nworkflow conventions.\n\n${REDIRECT_NOTE}\n`; + await writeFileGuarded(join(targetDir, ".cursor", "rules", "standards.mdc"), content, overwrite); + } + + if (options.agents.includes("windsurf")) { + const content = `# Windsurf Agent Rules\n\nFollow the rules defined in AGENTS.md. That file is the single source of truth\nfor all coding standards, architecture decisions, and workflow conventions.\n\n${REDIRECT_NOTE}\n`; + await writeFileGuarded(join(targetDir, ".windsurfrules"), content, overwrite); + } + + if (options.agents.includes("copilot")) { + const content = `# GitHub Copilot Instructions\n\nRead and apply \`AGENTS.md\` for project conventions and workflow rules.\n\n${REDIRECT_NOTE}\n`; + await writeFileGuarded(join(targetDir, ".github", "copilot-instructions.md"), content, overwrite); + } + + if (options.agents.includes("devin")) { + const devinJson = { + agent_instructions: "AGENTS.md", + repo_notes: { + content: `This project uses the agentic-context MCP server for on-demand standards and playbooks. Call its tools (search, get_standard, get_playbook, get_convention) before starting any task. AGENTS.md is the single source of truth for project conventions.`, + }, + }; + await writeFileGuarded( + join(targetDir, ".devin", "devin.json"), + JSON.stringify(devinJson, null, 2) + "\n", + overwrite + ); + } +} + +async function writeMcpConfigs(options: InitOptions): Promise { + const { targetDir, agents } = options; + const serverConfig = buildServerConfig(options); + + if (agents.includes("claude")) { + await mergeMcpConfig(join(targetDir, ".mcp.json"), "mcpServers", "agentic-context", serverConfig); + } + + if (agents.includes("cursor")) { + await mergeMcpConfig(join(targetDir, ".cursor", "mcp.json"), "mcpServers", "agentic-context", serverConfig); + } + + if (agents.includes("copilot")) { + await mergeMcpConfig(join(targetDir, ".vscode", "mcp.json"), "servers", "agentic-context", serverConfig); + } + + if (agents.includes("devin")) { + await mergeMcpConfig(join(targetDir, ".devin", "mcp_config.json"), "mcpServers", "agentic-context", serverConfig); + } + + if (agents.includes("windsurf")) { + // Windsurf's MCP config is global (~/.codeium/windsurf/mcp_config.json), not + // project-scoped — writing to a user's home directory from a project-scoped + // CLI is surprising, so print the snippet instead of writing it. + console.error( + "[init] Windsurf reads MCP config from ~/.codeium/windsurf/mcp_config.json (global, not project-scoped)." + ); + console.error("[init] Add this entry manually (or via Windsurf's Settings > Tools > Add Server):"); + console.error( + JSON.stringify({ mcpServers: { "agentic-context": serverConfig } }, null, 2) + ); + } +} + +export async function runInit(argv: string[]): Promise { + if (argv.includes("--help") || argv.includes("-h")) { + console.log(INIT_HELP); + return; + } + + const options = parseArgs(argv); + const source = new ContentSource({ + baseUrl: options.contentBaseUrl, + sourceType: options.sourceType, + azureDevOps: { + organization: options.azureOrg, + project: options.azureProject, + repository: options.azureRepo, + branch: options.azureBranch, + }, + }); + + console.error(`[init] Target directory: ${options.targetDir}`); + console.error(`[init] Agents: ${options.agents.join(", ")}`); + console.error(`[init] Content source: ${source.describeSource()}`); + console.error(""); + + await writeAgentsMd(source, options); + await writeAgentRedirects(options); + await writeAzureCredentialsFiles(options); + await writeMcpConfigs(options); + + console.error(""); + console.error("[init] Done. Next steps:"); + console.error(" 1. Fill in the [CONFIGURE] sections in AGENTS.md."); + if (options.sourceType === "azure-devops") { + console.error( + " 2. Paste your Azure DevOps PAT (Code (Read) scope) into .devin/credentials/agentic-context in this repo, replacing the instructions there. It is already gitignored." + ); + console.error(" 3. Restart your agent so it picks up the new MCP server registration."); + console.error(" 4. Verify with a search, e.g. ask your agent to search for \"security\"."); + } else { + console.error(" 2. Restart your agent so it picks up the new MCP server registration."); + console.error(" 3. Verify with a search, e.g. ask your agent to search for \"security\"."); + } +} diff --git a/mcp-server/src/server.ts b/mcp-server/src/server.ts new file mode 100644 index 0000000..0fb37c8 --- /dev/null +++ b/mcp-server/src/server.ts @@ -0,0 +1,383 @@ +/** + * Agentic Context MCP Server + * + * Serves standards, playbooks, and conventions from the agentic-context + * repository via the Model Context Protocol — fetched over plain HTTPS at + * request time and cached in memory. No git clone, no build-time bundling. + * + * Content source: + * - CONTENT_BASE_URL env var (optional) — point at a team's own published + * fork (Azure Static Web Apps, Blob Storage static site, GitHub Pages, + * etc.), as long as it mirrors this repo's file layout. + * - CONTENT_SOURCE_TYPE=azure-devops (optional) — fetch from a private + * Azure Repos repo via the Git Items REST API instead. Requires + * AZURE_DEVOPS_ORG, AZURE_DEVOPS_PROJECT, AZURE_DEVOPS_REPO, and + * AZURE_DEVOPS_PAT (a PAT with Code (Read) scope); AZURE_DEVOPS_BRANCH + * defaults to "main". + * - Defaults to raw.githubusercontent.com for the upstream repo's main + * branch. + * - CACHE_TTL_MINUTES env var (optional, default 240 / 4 hours) controls + * how long fetched content is cached before being re-fetched. + * + * Transport: stdio (default for local MCP servers) + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { ContentSource } from "./content.js"; + +export async function startServer(): Promise { + const source = new ContentSource(); + + const server = new McpServer({ + name: "agentic-context", + version: "0.1.0", + }); + + // --------------------------------------------------------------------------- + // Tool: search + // --------------------------------------------------------------------------- + server.registerTool( + "search", + { + description: + "Search the agentic-context knowledge base by keywords. Returns matching standards, playbooks, and conventions ranked by relevance. Use this when starting a task to find which standards and playbooks apply.", + inputSchema: { + query: z + .string() + .describe( + "Space-separated keywords to search for (e.g. 'security OWASP', 'refactor code smell', 'testing coverage')" + ), + }, + }, + async ({ query }) => { + try { + const results = await source.search(query); + if (results.length === 0) { + return { + content: [ + { + type: "text" as const, + text: `No matches found for "${query}". Try broader terms or use list_standards / list_playbooks to see all available content.`, + }, + ], + }; + } + + const formatted = results + .map( + (r) => + `[${r.relevance.toUpperCase()}] ${r.category}: ${r.name}${r.subcategory ? ` (${r.subcategory})` : ""}\n Keywords: ${r.keywords} | Summary: ${r.summary}\n -> Use get_${r.category} to load the full content` + ) + .join("\n\n"); + + return { + content: [ + { + type: "text" as const, + text: `Found ${results.length} matches for "${query}":\n\n${formatted}`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: "text" as const, text: `Search error: ${err}` }], + isError: true, + }; + } + } + ); + + // --------------------------------------------------------------------------- + // Tool: get_index + // --------------------------------------------------------------------------- + server.registerTool( + "get_index", + { + description: + "Get the full context index — the keyword routing table that maps task keywords to standards, playbooks, and conventions. Load this first to understand what content is available.", + inputSchema: {}, + }, + async () => { + try { + const index = await source.getIndex(); + return { content: [{ type: "text" as const, text: index }] }; + } catch (err) { + return { + content: [{ type: "text" as const, text: `Error loading index: ${err}` }], + isError: true, + }; + } + } + ); + + // --------------------------------------------------------------------------- + // Tool: get_agents_config + // --------------------------------------------------------------------------- + server.registerTool( + "get_agents_config", + { + description: + "Get the AGENTS.md template — the lean project configuration file with mandated standards, core principles, and [CONFIGURE] sections for project-specific setup.", + inputSchema: {}, + }, + async () => { + try { + const config = await source.getAgentsConfig(); + return { content: [{ type: "text" as const, text: config }] }; + } catch (err) { + return { + content: [{ type: "text" as const, text: `Error loading AGENTS.md: ${err}` }], + isError: true, + }; + } + } + ); + + // --------------------------------------------------------------------------- + // Tool: list_standards + // --------------------------------------------------------------------------- + server.registerTool( + "list_standards", + { + description: + "List all available coding standards (e.g. security, testing, code-quality, dotnet, react). Returns names and descriptions.", + inputSchema: {}, + }, + async () => { + try { + const standards = await source.listStandards(); + const formatted = standards.map((s) => `- ${s.name}: ${s.summary}`).join("\n"); + return { + content: [ + { + type: "text" as const, + text: `Available standards (${standards.length}):\n\n${formatted}\n\nUse get_standard with the name to load the full content.`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: "text" as const, text: `Error listing standards: ${err}` }], + isError: true, + }; + } + } + ); + + // --------------------------------------------------------------------------- + // Tool: get_standard + // --------------------------------------------------------------------------- + server.registerTool( + "get_standard", + { + description: + "Get the full content of a specific coding standard. Standards contain prescriptive rules with Non-Negotiables and Decision Checklists.", + inputSchema: { + name: z + .string() + .describe( + "Standard name without .md extension (e.g. 'security', 'testing', 'code-quality', 'dotnet', 'react')" + ), + }, + }, + async ({ name }) => { + try { + const content = await source.getStandard(name); + return { content: [{ type: "text" as const, text: content }] }; + } catch { + const standards = await source.listStandards(); + const available = standards.map((s) => s.name).join(", "); + return { + content: [ + { + type: "text" as const, + text: `Standard "${name}" not found. Available standards: ${available}`, + }, + ], + isError: true, + }; + } + } + ); + + // --------------------------------------------------------------------------- + // Tool: list_playbooks + // --------------------------------------------------------------------------- + server.registerTool( + "list_playbooks", + { + description: + "List all available playbooks organised by category (assess, review, plan, refactor, docs, setup). Returns names, categories, and descriptions.", + inputSchema: { + category: z + .string() + .optional() + .describe( + "Optional category filter: 'assess', 'review', 'plan', 'refactor', 'docs', or 'setup'" + ), + }, + }, + async ({ category }) => { + try { + const playbooks = await source.listPlaybooks(category); + + const grouped: Record = {}; + for (const p of playbooks) { + const cat = p.subcategory || "other"; + if (!grouped[cat]) grouped[cat] = []; + grouped[cat].push(p); + } + + const formatted = Object.entries(grouped) + .map(([cat, items]) => { + const list = items.map((p) => ` - ${p.name}: ${p.summary}`).join("\n"); + return `### ${cat}\n${list}`; + }) + .join("\n\n"); + + return { + content: [ + { + type: "text" as const, + text: `Available playbooks (${playbooks.length}):\n\n${formatted}\n\nUse get_playbook with category and name to load the full content.`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: "text" as const, text: `Error listing playbooks: ${err}` }], + isError: true, + }; + } + } + ); + + // --------------------------------------------------------------------------- + // Tool: get_playbook + // --------------------------------------------------------------------------- + server.registerTool( + "get_playbook", + { + description: + "Get the full content of a specific playbook. Playbooks are step-by-step procedures for assessments, reviews, planning, refactoring, docs generation, or setup tasks.", + inputSchema: { + category: z + .string() + .describe( + "Playbook category: 'assess', 'review', 'plan', 'refactor', 'docs', or 'setup'" + ), + name: z + .string() + .describe( + "Playbook name without .md extension (e.g. 'security', 'code-quality', 'safe-refactor')" + ), + }, + }, + async ({ category, name }) => { + try { + const content = await source.getPlaybook(category, name); + return { content: [{ type: "text" as const, text: content }] }; + } catch { + const playbooks = await source.listPlaybooks(); + const inCategory = playbooks.filter((p) => p.subcategory === category); + if (inCategory.length > 0) { + const available = inCategory.map((p) => p.name).join(", "); + return { + content: [ + { + type: "text" as const, + text: `Playbook "${name}" not found in category "${category}". Available in ${category}: ${available}`, + }, + ], + isError: true, + }; + } + const categories = [...new Set(playbooks.map((p) => p.subcategory))].join(", "); + return { + content: [ + { + type: "text" as const, + text: `Category "${category}" not found. Available categories: ${categories}`, + }, + ], + isError: true, + }; + } + } + ); + + // --------------------------------------------------------------------------- + // Tool: list_conventions + // --------------------------------------------------------------------------- + server.registerTool( + "list_conventions", + { + description: + "List all available conventions (code, workflow, communication). These are style and process guidance documents.", + inputSchema: {}, + }, + async () => { + try { + const conventions = await source.listConventions(); + const formatted = conventions.map((c) => `- ${c.name}: ${c.summary}`).join("\n"); + return { + content: [ + { + type: "text" as const, + text: `Available conventions (${conventions.length}):\n\n${formatted}\n\nUse get_convention with the name to load the full content.`, + }, + ], + }; + } catch (err) { + return { + content: [{ type: "text" as const, text: `Error listing conventions: ${err}` }], + isError: true, + }; + } + } + ); + + // --------------------------------------------------------------------------- + // Tool: get_convention + // --------------------------------------------------------------------------- + server.registerTool( + "get_convention", + { + description: + "Get the full content of a specific convention document (code, workflow, or communication).", + inputSchema: { + name: z + .string() + .describe( + "Convention name without .md extension: 'code', 'workflow', or 'communication'" + ), + }, + }, + async ({ name }) => { + try { + const content = await source.getConvention(name); + return { content: [{ type: "text" as const, text: content }] }; + } catch { + const conventions = await source.listConventions(); + const available = conventions.map((c) => c.name).join(", "); + return { + content: [ + { + type: "text" as const, + text: `Convention "${name}" not found. Available conventions: ${available}`, + }, + ], + isError: true, + }; + } + } + ); + + const transport = new StdioServerTransport(); + await server.connect(transport); + // All logging must go to stderr — stdout is the MCP JSON-RPC channel + console.error("[agentic-context-mcp] Server started"); + console.error(`[agentic-context-mcp] Content source: ${source.describeSource()}`); +} diff --git a/mcp-server/test/cli.test.ts b/mcp-server/test/cli.test.ts new file mode 100644 index 0000000..b06ae6d --- /dev/null +++ b/mcp-server/test/cli.test.ts @@ -0,0 +1,204 @@ +/** + * Black-box tests for the built CLI binary (build/cli.js), spawned as a real + * child process — mirroring the style of the repo's own tests/test-deploy.sh + * (invoke the real script, assert on real output/files) rather than testing + * internals directly. Requires `npm run build` to have run first (see the + * `pretest` script in package.json). + */ + +import { test, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { mkdtemp, readFile, rm, access } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { startFixtureServer, type FixtureServer } from "./helpers/fixture-server.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = join(__dirname, "fixtures", "repo"); +const CLI_PATH = join(__dirname, "..", "build", "cli.js"); + +let fixtureServer: FixtureServer; + +before(async () => { + try { + await access(CLI_PATH); + } catch { + throw new Error(`${CLI_PATH} not found — run "npm run build" before the test suite.`); + } + fixtureServer = await startFixtureServer(FIXTURES_DIR); +}); + +after(async () => { + await fixtureServer.close(); +}); + +interface JsonRpcExchange { + stdout: string; + stderr: string; +} + +/** Spawn the CLI, write newline-delimited JSON-RPC requests to stdin, and + * collect everything written to stdout/stderr before the process exits. */ +function runServerRequests(requests: object[], env: Record = {}): Promise { + return new Promise((resolvePromise, reject) => { + const child = spawn(process.execPath, [CLI_PATH], { + env: { ...process.env, ...env }, + }); + + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => (stdout += chunk)); + child.stderr.on("data", (chunk) => (stderr += chunk)); + + const timeout = setTimeout(() => { + child.kill(); + reject(new Error(`Timed out waiting for CLI response. stderr so far: ${stderr}`)); + }, 10000); + + child.on("error", (err) => { + clearTimeout(timeout); + reject(err); + }); + + // Give the process a moment to start listening, then send requests and + // close stdin so it doesn't hang around after replying. + setTimeout(() => { + for (const req of requests) { + child.stdin.write(JSON.stringify(req) + "\n"); + } + child.stdin.end(); + }, 300); + + child.on("close", () => { + clearTimeout(timeout); + resolvePromise({ stdout, stderr }); + }); + }); +} + +function parseJsonRpcLines(stdout: string): any[] { + return stdout + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => JSON.parse(line)); +} + +test("starting with no subcommand serves the MCP protocol over stdio", async () => { + const { stdout, stderr } = await runServerRequests( + [ + { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0.0" } }, + }, + { jsonrpc: "2.0", method: "notifications/initialized" }, + { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }, + ], + { CONTENT_BASE_URL: fixtureServer.url } + ); + + const messages = parseJsonRpcLines(stdout); + const initResponse = messages.find((m) => m.id === 1); + assert.equal(initResponse.result.serverInfo.name, "agentic-context"); + + const toolsResponse = messages.find((m) => m.id === 2); + const toolNames = toolsResponse.result.tools.map((t: { name: string }) => t.name).sort(); + assert.deepEqual(toolNames, [ + "get_agents_config", + "get_convention", + "get_index", + "get_playbook", + "get_standard", + "list_conventions", + "list_playbooks", + "list_standards", + "search", + ]); + + assert.match(stderr, /Server started/); + assert.match(stderr, new RegExp(fixtureServer.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); +}); + +test("search tool call returns matches fetched from CONTENT_BASE_URL", async () => { + const { stdout } = await runServerRequests( + [ + { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0.0" } }, + }, + { jsonrpc: "2.0", method: "notifications/initialized" }, + { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "search", arguments: { query: "security" } } }, + ], + { CONTENT_BASE_URL: fixtureServer.url } + ); + + const messages = parseJsonRpcLines(stdout); + const searchResponse = messages.find((m) => m.id === 2); + const text = searchResponse.result.content[0].text; + assert.match(text, /standard: security/); +}); + +/** Spawn the CLI with plain args and collect stdout/stderr/exit code — for + * one-shot commands like `--help` that don't speak the MCP protocol. */ +function runCli(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> { + return new Promise((resolvePromise, reject) => { + const child = spawn(process.execPath, [CLI_PATH, ...args]); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => (stdout += chunk)); + child.stderr.on("data", (chunk) => (stderr += chunk)); + child.on("error", reject); + child.on("close", (code) => resolvePromise({ stdout, stderr, exitCode: code ?? 1 })); + }); +} + +test("--help prints usage without starting the server", async () => { + const { stdout, stderr, exitCode } = await runCli(["--help"]); + assert.equal(exitCode, 0); + assert.match(stdout, /Usage:/); + assert.match(stdout, /agentic-context-mcp init \[options\]/); + assert.doesNotMatch(stderr, /Server started/); +}); + +test("-h is a shorthand for --help", async () => { + const { stdout, exitCode } = await runCli(["-h"]); + assert.equal(exitCode, 0); + assert.match(stdout, /Usage:/); +}); + +test("init --help prints init's options without touching the network or filesystem", async () => { + const { stdout, stderr, exitCode } = await runCli(["init", "--help"]); + assert.equal(exitCode, 0); + assert.match(stdout, /agentic-context-mcp init \[target-dir\] \[options\]/); + assert.match(stdout, /--source-type /); + assert.match(stdout, /--azure-org /); + assert.match(stdout, /AZURE_DEVOPS_PAT/); + assert.doesNotMatch(stderr, /\[init\]/); // no actual init work happened +}); + +test("`init` subcommand writes real files when invoked as a CLI", async () => { + const dir = await mkdtemp(join(tmpdir(), "agentic-context-cli-init-")); + try { + const exitCode = await new Promise((resolvePromise, reject) => { + const child = spawn( + process.execPath, + [CLI_PATH, "init", dir, "--agents", "claude", "--content-base-url", fixtureServer.url], + { stdio: ["ignore", "ignore", "ignore"] } + ); + child.on("error", reject); + child.on("close", (code) => resolvePromise(code ?? 1)); + }); + + assert.equal(exitCode, 0); + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + assert.match(agentsMd, /agentic-context` MCP server/); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/mcp-server/test/content-stale-fallback.test.ts b/mcp-server/test/content-stale-fallback.test.ts new file mode 100644 index 0000000..19e4489 --- /dev/null +++ b/mcp-server/test/content-stale-fallback.test.ts @@ -0,0 +1,28 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { ContentSource } from "../src/content.js"; +import { startFixtureServer } from "./helpers/fixture-server.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = join(__dirname, "fixtures", "repo"); + +// Isolated in its own file/server (rather than sharing content.test.ts's +// fixture server) because it deliberately takes the server down mid-test. +test("serves stale cached content when a refetch fails instead of throwing", async () => { + const server = await startFixtureServer(FIXTURES_DIR); + try { + const source = new ContentSource({ baseUrl: server.url, cacheTtlMinutes: 1 / 60000 }); // ~1ms + const first = await source.getConvention("code"); + + await server.close(); + await new Promise((r) => setTimeout(r, 30)); + + const second = await source.getConvention("code"); + assert.equal(second, first); + } finally { + // server may already be closed; ignore double-close errors + await server.close().catch(() => {}); + } +}); diff --git a/mcp-server/test/content.test.ts b/mcp-server/test/content.test.ts new file mode 100644 index 0000000..0540053 --- /dev/null +++ b/mcp-server/test/content.test.ts @@ -0,0 +1,179 @@ +import { test, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { ContentSource } from "../src/content.js"; +import { AZURE_DEVOPS_PAT_INSTRUCTIONS } from "../src/fetchers.js"; +import { startFixtureServer, type FixtureServer } from "./helpers/fixture-server.js"; + +async function withTempDir(fn: (dir: string) => Promise): Promise { + const dir = await mkdtemp(join(tmpdir(), "agentic-context-content-")); + try { + await fn(dir); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = join(__dirname, "fixtures", "repo"); + +let fixtureServer: FixtureServer; + +before(async () => { + fixtureServer = await startFixtureServer(FIXTURES_DIR); +}); + +after(async () => { + await fixtureServer.close(); +}); + +test("getIndex fetches the context index", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const index = await source.getIndex(); + assert.match(index, /Context Index/); +}); + +test("getAgentsConfig fetches AGENTS.md", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const agents = await source.getAgentsConfig(); + assert.match(agents, /## Context System/); +}); + +test("getStandard fetches a standard's full content by name", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const content = await source.getStandard("security"); + assert.match(content, /OWASP Top 10/); +}); + +test("getStandard rejects for a standard that doesn't exist", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + await assert.rejects(() => source.getStandard("does-not-exist")); +}); + +test("getPlaybook fetches a playbook by category and name", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const content = await source.getPlaybook("assess", "security"); + assert.match(content, /Principal Security Engineer/); +}); + +test("getConvention fetches a convention by name", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const content = await source.getConvention("code"); + assert.match(content, /Simplicity First/); +}); + +test("listStandards derives entries from the index manifest", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const standards = await source.listStandards(); + const names = standards.map((s) => s.name).sort(); + assert.deepEqual(names, ["security", "testing"]); + assert.ok(standards.every((s) => s.category === "standard")); +}); + +test("listPlaybooks filters by category and sets subcategory", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const assessPlaybooks = await source.listPlaybooks("assess"); + assert.equal(assessPlaybooks.length, 1); + assert.equal(assessPlaybooks[0].name, "security"); + assert.equal(assessPlaybooks[0].subcategory, "assess"); + + const allPlaybooks = await source.listPlaybooks(); + assert.equal(allPlaybooks.length, 2); +}); + +test("listConventions derives entries from the index manifest", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const conventions = await source.listConventions(); + assert.equal(conventions.length, 3); + assert.ok(conventions.some((c) => c.name === "code")); +}); + +test("search ranks matches by how many terms hit the keyword list", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const results = await source.search("security OWASP"); + assert.ok(results.length > 0); + assert.equal(results[0].relevance, "high"); + assert.ok(results.some((r) => r.category === "standard" && r.name === "security")); +}); + +test("search returns an empty array when nothing matches", async () => { + const source = new ContentSource({ baseUrl: fixtureServer.url }); + const results = await source.search("nonexistent-zzz-term"); + assert.equal(results.length, 0); +}); + +test("caches fetched content within the TTL window", async () => { + const before = fixtureServer.requestCounts["standards/security.md"] || 0; + const source = new ContentSource({ baseUrl: fixtureServer.url, cacheTtlMinutes: 60 }); + await source.getStandard("security"); + await source.getStandard("security"); + assert.equal(fixtureServer.requestCounts["standards/security.md"] - before, 1); +}); + +test("re-fetches once the cache TTL has expired", async () => { + const before = fixtureServer.requestCounts["standards/testing.md"] || 0; + const source = new ContentSource({ baseUrl: fixtureServer.url, cacheTtlMinutes: 1 / 60000 }); // ~1ms + await source.getStandard("testing"); + await new Promise((r) => setTimeout(r, 30)); + await source.getStandard("testing"); + assert.equal(fixtureServer.requestCounts["standards/testing.md"] - before, 2); +}); + +test("the manifest itself is cached, not re-fetched per list call", async () => { + const before = fixtureServer.requestCounts["core/.context/index.md"] || 0; + const source = new ContentSource({ baseUrl: fixtureServer.url, cacheTtlMinutes: 60 }); + await source.listStandards(); + await source.listPlaybooks(); + await source.listConventions(); + await source.search("security"); + assert.equal(fixtureServer.requestCounts["core/.context/index.md"] - before, 1); +}); + +test("ContentSource starts and surfaces a clear error when the Azure DevOps PAT file is missing", async () => { + await withTempDir(async (dir) => { + const originalCwd = process.cwd(); + const originalEnv = { ...process.env }; + process.chdir(dir); + try { + process.env.CONTENT_SOURCE_TYPE = "azure-devops"; + process.env.AZURE_DEVOPS_ORG = "env-org"; + process.env.AZURE_DEVOPS_PROJECT = "env-project"; + process.env.AZURE_DEVOPS_REPO = "env-repo"; + process.env.AZURE_DEVOPS_PAT = "${file:missing-credentials}"; + + const source = new ContentSource(); + assert.match(source.describeSource(), /unconfigured/); + await assert.rejects(() => source.getIndex(), /missing-credentials/); + } finally { + process.chdir(originalCwd); + process.env = originalEnv; + } + }); +}); + +test("ContentSource starts and surfaces a clear error when the Azure DevOps PAT file still has placeholder instructions", async () => { + await withTempDir(async (dir) => { + await writeFile(join(dir, "agentic-context"), AZURE_DEVOPS_PAT_INSTRUCTIONS, "utf-8"); + + const originalCwd = process.cwd(); + const originalEnv = { ...process.env }; + process.chdir(dir); + try { + process.env.CONTENT_SOURCE_TYPE = "azure-devops"; + process.env.AZURE_DEVOPS_ORG = "env-org"; + process.env.AZURE_DEVOPS_PROJECT = "env-project"; + process.env.AZURE_DEVOPS_REPO = "env-repo"; + process.env.AZURE_DEVOPS_PAT = "${file:agentic-context}"; + + const source = new ContentSource(); + assert.match(source.describeSource(), /placeholder/); + await assert.rejects(() => source.getAgentsConfig(), /placeholder/); + } finally { + process.chdir(originalCwd); + process.env = originalEnv; + } + }); +}); diff --git a/mcp-server/test/fetchers.test.ts b/mcp-server/test/fetchers.test.ts new file mode 100644 index 0000000..ec58c35 --- /dev/null +++ b/mcp-server/test/fetchers.test.ts @@ -0,0 +1,226 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { AzureDevOpsFetcher, RawUrlFetcher, createFetcher } from "../src/fetchers.js"; + +async function withTempDir(fn: (dir: string) => Promise): Promise { + const dir = await mkdtemp(join(tmpdir(), "agentic-context-fetchers-")); + try { + await fn(dir); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** Minimal Response stand-in so tests don't need a real network call. */ +function fakeResponse(): Response { + return new Response("content", { status: 200 }); +} + +test("RawUrlFetcher concatenates base URL and relative path", async () => { + const requested: Array<{ url: string; headers: Headers }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (url: string, init?: RequestInit) => { + requested.push({ url, headers: new Headers(init?.headers) }); + return fakeResponse(); + }) as typeof fetch; + + try { + const fetcher = new RawUrlFetcher("https://example.com/repo"); + await fetcher.fetch("standards/security.md"); + assert.equal(requested[0].url, "https://example.com/repo/standards/security.md"); + assert.equal(requested[0].headers.get("authorization"), null); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("RawUrlFetcher sends a bearer token when one is configured", async () => { + const requested: Array<{ headers: Headers }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (_url: string, init?: RequestInit) => { + requested.push({ headers: new Headers(init?.headers) }); + return fakeResponse(); + }) as typeof fetch; + + try { + const fetcher = new RawUrlFetcher("https://example.com/repo", "secret-token"); + await fetcher.fetch("standards/security.md"); + assert.equal(requested[0].headers.get("authorization"), "Bearer secret-token"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("RawUrlFetcher.describe returns the base URL", () => { + const fetcher = new RawUrlFetcher("https://example.com/repo"); + assert.equal(fetcher.describe(), "https://example.com/repo"); +}); + +test("AzureDevOpsFetcher builds the Git Items API URL with query params and Basic auth", async () => { + const requested: Array<{ url: string; headers: Headers }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (url: string, init?: RequestInit) => { + requested.push({ url, headers: new Headers(init?.headers) }); + return fakeResponse(); + }) as typeof fetch; + + try { + const fetcher = new AzureDevOpsFetcher({ + organization: "my-org", + project: "my-project", + repository: "agentic-context", + pat: "my-pat", + }); + await fetcher.fetch("standards/security.md"); + + const url = new URL(requested[0].url); + assert.equal(url.origin, "https://dev.azure.com"); + assert.equal(url.pathname, "/my-org/my-project/_apis/git/repositories/agentic-context/items"); + assert.equal(url.searchParams.get("path"), "/standards/security.md"); + assert.equal(url.searchParams.get("versionDescriptor.version"), "main"); + assert.equal(url.searchParams.get("api-version"), "7.1"); + assert.equal(url.searchParams.get("$format"), "text"); + + const expectedAuth = `Basic ${Buffer.from(":my-pat").toString("base64")}`; + assert.equal(requested[0].headers.get("authorization"), expectedAuth); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("AzureDevOpsFetcher honours a custom branch and api version", async () => { + const requested: string[] = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (url: string) => { + requested.push(url); + return fakeResponse(); + }) as typeof fetch; + + try { + const fetcher = new AzureDevOpsFetcher({ + organization: "my-org", + project: "my-project", + repository: "agentic-context", + pat: "my-pat", + branch: "release", + apiVersion: "7.0", + }); + await fetcher.fetch("core/AGENTS.md"); + + const url = new URL(requested[0]); + assert.equal(url.searchParams.get("versionDescriptor.version"), "release"); + assert.equal(url.searchParams.get("api-version"), "7.0"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("AzureDevOpsFetcher.describe summarises org/project/repo/branch", () => { + const fetcher = new AzureDevOpsFetcher({ + organization: "my-org", + project: "my-project", + repository: "agentic-context", + pat: "my-pat", + branch: "release", + }); + assert.equal(fetcher.describe(), "azure-devops:my-org/my-project/agentic-context@release"); +}); + +test("createFetcher builds a RawUrlFetcher when baseUrl is passed explicitly", () => { + const fetcher = createFetcher({ baseUrl: "https://example.com/repo/" }); + assert.ok(fetcher instanceof RawUrlFetcher); + assert.equal(fetcher.describe(), "https://example.com/repo"); // trailing slash stripped +}); + +test("createFetcher builds an AzureDevOpsFetcher when sourceType is azure-devops", () => { + const fetcher = createFetcher({ + sourceType: "azure-devops", + azureDevOps: { + organization: "my-org", + project: "my-project", + repository: "agentic-context", + pat: "my-pat", + }, + }); + assert.ok(fetcher instanceof AzureDevOpsFetcher); + assert.equal(fetcher.describe(), "azure-devops:my-org/my-project/agentic-context@main"); +}); + +test("createFetcher throws a clear error when required Azure DevOps settings are missing", () => { + assert.throws( + () => createFetcher({ sourceType: "azure-devops", azureDevOps: { organization: "my-org" } }), + /AZURE_DEVOPS_PROJECT.*AZURE_DEVOPS_REPO.*AZURE_DEVOPS_PAT/ + ); +}); + +test("createFetcher reads Azure DevOps settings from environment variables as a fallback", () => { + const originalEnv = { ...process.env }; + process.env.CONTENT_SOURCE_TYPE = "azure-devops"; + process.env.AZURE_DEVOPS_ORG = "env-org"; + process.env.AZURE_DEVOPS_PROJECT = "env-project"; + process.env.AZURE_DEVOPS_REPO = "env-repo"; + process.env.AZURE_DEVOPS_PAT = "env-pat"; + + try { + const fetcher = createFetcher(); + assert.equal(fetcher.describe(), "azure-devops:env-org/env-project/env-repo@main"); + } finally { + process.env = originalEnv; + } +}); + +test("createFetcher expands a ${file:path} reference in AZURE_DEVOPS_PAT", async () => { + await withTempDir(async (dir) => { + await writeFile(join(dir, "agentic-context"), "file-pat-value\n", "utf-8"); + + const requested: Array<{ headers: Headers }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (_url: string, init?: RequestInit) => { + requested.push({ headers: new Headers(init?.headers) }); + return new Response("content", { status: 200 }); + }) as typeof fetch; + + const originalCwd = process.cwd(); + const originalEnv = { ...process.env }; + process.chdir(dir); + try { + process.env.CONTENT_SOURCE_TYPE = "azure-devops"; + process.env.AZURE_DEVOPS_ORG = "env-org"; + process.env.AZURE_DEVOPS_PROJECT = "env-project"; + process.env.AZURE_DEVOPS_REPO = "env-repo"; + process.env.AZURE_DEVOPS_PAT = "${file:agentic-context}"; + + const fetcher = createFetcher(); + await fetcher.fetch("core/AGENTS.md"); + const expectedAuth = `Basic ${Buffer.from(":file-pat-value").toString("base64")}`; + assert.equal(requested[0].headers.get("authorization"), expectedAuth); + } finally { + process.chdir(originalCwd); + process.env = originalEnv; + globalThis.fetch = originalFetch; + } + }); +}); + +test("createFetcher throws a clear error when a ${file:path} reference cannot be read", async () => { + await withTempDir(async (dir) => { + const originalCwd = process.cwd(); + const originalEnv = { ...process.env }; + process.chdir(dir); + try { + process.env.CONTENT_SOURCE_TYPE = "azure-devops"; + process.env.AZURE_DEVOPS_ORG = "env-org"; + process.env.AZURE_DEVOPS_PROJECT = "env-project"; + process.env.AZURE_DEVOPS_REPO = "env-repo"; + process.env.AZURE_DEVOPS_PAT = "${file:missing-pat.txt}"; + + assert.throws(() => createFetcher(), /missing-pat\.txt/); + } finally { + process.chdir(originalCwd); + process.env = originalEnv; + } + }); +}); diff --git a/mcp-server/test/fixtures/repo/core/.context/conventions/code.md b/mcp-server/test/fixtures/repo/core/.context/conventions/code.md new file mode 100644 index 0000000..3584fbb --- /dev/null +++ b/mcp-server/test/fixtures/repo/core/.context/conventions/code.md @@ -0,0 +1,5 @@ +# Code Conventions (fixture) + +## Core Principles + +- Simplicity First diff --git a/mcp-server/test/fixtures/repo/core/.context/conventions/communication.md b/mcp-server/test/fixtures/repo/core/.context/conventions/communication.md new file mode 100644 index 0000000..f4c2cb0 --- /dev/null +++ b/mcp-server/test/fixtures/repo/core/.context/conventions/communication.md @@ -0,0 +1,5 @@ +# Communication Conventions (fixture) + +## Writing Standards + +Concise, active voice, British English. diff --git a/mcp-server/test/fixtures/repo/core/.context/conventions/workflow.md b/mcp-server/test/fixtures/repo/core/.context/conventions/workflow.md new file mode 100644 index 0000000..3f8487f --- /dev/null +++ b/mcp-server/test/fixtures/repo/core/.context/conventions/workflow.md @@ -0,0 +1,5 @@ +# Workflow Conventions (fixture) + +## Plan Mode Default + +Enter plan mode for non-trivial tasks. diff --git a/mcp-server/test/fixtures/repo/core/.context/index.md b/mcp-server/test/fixtures/repo/core/.context/index.md new file mode 100644 index 0000000..afd27d8 --- /dev/null +++ b/mcp-server/test/fixtures/repo/core/.context/index.md @@ -0,0 +1,39 @@ +# Context Index + +Before starting any task, scan this index for matching keywords. +Load the referenced files into your context before proceeding. + +--- + +## Standards (reference — load when working in the domain) + +| Keywords | File | Summary | +|----------|------|---------| +| security, OWASP, vulnerability, injection, auth, secrets | `.context/standards/security.md` | OWASP Top 10 compliance | +| testing, test, coverage, TDD, unit test, integration test | `.context/standards/testing.md` | Test Trophy Model, >= 90% coverage | + +--- + +## Playbooks — Assessments (structured codebase-level evaluation) + +| Keywords | File | Summary | +|----------|------|---------| +| assess security, security audit, threat model, owasp top 10 | `.context/playbooks/assess/security.md` | OWASP Top 10 security assessment | + +--- + +## Playbooks — Reviews (PR-level or change-level evaluation) + +| Keywords | File | Summary | +|----------|------|---------| +| review security, security review | `.context/playbooks/review/security.md` | OWASP, secrets, injection vectors | + +--- + +## Conventions (style and workflow guidance) + +| Keywords | File | Summary | +|----------|------|---------| +| naming, conventions, patterns, imports, file handling | `.context/conventions/code.md` | Naming, patterns, imports, core principles | +| workflow, planning, tasks, plan mode, verification | `.context/conventions/workflow.md` | Plan-first, task management, verification | +| writing, communication, style, tone, British English | `.context/conventions/communication.md` | Research, writing, and communication standards | diff --git a/mcp-server/test/fixtures/repo/core/AGENTS.md b/mcp-server/test/fixtures/repo/core/AGENTS.md new file mode 100644 index 0000000..1e68a7d --- /dev/null +++ b/mcp-server/test/fixtures/repo/core/AGENTS.md @@ -0,0 +1,170 @@ +# AGENTS.md + + comments after populating. --> + +## Project Overview [CONFIGURE] + + + +--- + +## Tech Stack [CONFIGURE] + + + +- **Language(s):** +- **Framework(s):** +- **Database(s):** +- **Testing:** +- **Linting / Formatting:** +- **Package Manager:** + +--- + +## Commands [CONFIGURE] + +```bash + + + + + + + + +``` + +--- + +## Architecture [CONFIGURE] + + + +- **Style:** +- **Deployment model:** +- **Service boundaries:** + +### Dependency Direction + +Dependencies point inward. This is non-negotiable. + +```text +Presentation (Controllers / API) + ↓ +Application (Use Cases / Handlers) + ↓ +Domain (Entities / Value Objects / Interfaces) + ↓ +Infrastructure (Database / External APIs / Messaging) +``` + + + +### Key Design Decisions + + + +--- + +## Repository Structure [CONFIGURE] + + + +```text + +``` + +--- + +## Code Conventions [CONFIGURE] + +### Naming + + + +### Patterns + + + +### Import Rules + + + +--- + +## Context System + +This repository uses on-demand context loading. Before starting any task, read `.context/index.md` and load files matching the current task's domain. + +Available context types: + +- **Standards** in `.context/standards/` — detailed reference for a specific concern (security, testing, performance, etc.) +- **Playbooks** in `.context/playbooks/` — step-by-step procedures for assessments, reviews, plans, and refactoring +- **Conventions** in `.context/conventions/` — workflow, communication, and coding style guidance + +> This repository was set up with `deploy.sh`/`deploy.ps1`, which copies these files locally. If you instead configured the `agentic-context-mcp` MCP server (see the [agentic-context repo](https://github.com/ldastey-dev/agentic-context)'s `mcp-server/`), run `npx agentic-context-mcp init` to generate an MCP-flavoured `AGENTS.md` instead of using this one. + +--- + +## Mandated Standards + +The following standards are non-negotiable. Do not weaken them. Detailed guidance is in `.context/standards/`. + +### Core Principles + +- **Simplicity First:** Make every change as simple as possible. Impact minimal code. +- **No Laziness:** Find root causes. No temporary fixes. Senior developer standards. +- **Minimal Impact:** Changes should only touch what's necessary. Avoid introducing bugs. +- **Security is Non-Negotiable:** Never log secrets, commit credentials, or introduce injection vectors. +- **Test What You Change:** If you modify behaviour, prove it works. If you refactor, prove nothing broke. +- **Evidence Over Opinion:** Reference specific code, config, or behaviour. No vague assertions. + +### Domain Standards + +| Standard | Key Rule | Detail | +| --- | --- | --- | +| Code Quality | SOLID, DRY, cyclomatic complexity < 10 | `.context/standards/code-quality.md` | +| Security | OWASP Top 10 compliance | `.context/standards/security.md` | +| Testing | >= 90% coverage, Test Trophy Model | `.context/standards/testing.md` | +| CI/CD | 7-stage pipeline, < 10 min full CI | `.context/standards/ci-cd.md` | +| Observability | OpenTelemetry, structured JSON logging | `.context/standards/observability.md` | +| Resilience | Circuit breakers, retries with backoff | `.context/standards/resilience.md` | +| Performance | No N+1, pagination, resource disposal | `.context/standards/performance.md` | +| Cost | Cache before network, FinOps principles | `.context/standards/cost-optimisation.md` | +| Operations | IaC, env vars, small focused PRs | `.context/standards/operational-excellence.md` | +| API Design | OpenAPI 3+, REST, RFC 7807 errors | `.context/standards/api-design.md` | +| AWS | 6 pillars: OpEx, Security, Reliability, Perf, Cost, Sustainability | `.context/standards/aws-well-architected.md` | +| Azure | 5 pillars: Reliability, Security, Cost, OpEx, Performance | `.context/standards/azure-well-architected.md` | +| GDPR | Lawful basis, data minimisation, subject rights, cookies & tracking declaration | `.context/standards/gdpr.md` | +| PCI DSS | CDE scoping, AES-256, TLS 1.2+ | `.context/standards/pci-dss.md` | +| Accessibility | WCAG 2.2 AA, keyboard, ARIA, contrast | `.context/standards/accessibility.md` | +| Architecture | Clean Architecture, dependency direction, layer boundaries | `.context/standards/architecture.md` | +| IaC | State management, drift detection, container security | `.context/standards/iac.md` | +| Tech Debt | Debt taxonomy, impact scoring, paydown strategy | `.context/standards/tech-debt.md` | + +### Technology Standards + +| Standard | Key Rule | Detail | +| --- | --- | --- | +| .NET | C#, ASP.NET Core, EF Core, async patterns | `.context/standards/dotnet.md` | +| React | Component architecture, hooks, Testing Library | `.context/standards/react.md` | +| SQL Server | Schema design, migrations, Azure SQL | `.context/standards/mssql.md` | +| PowerShell | Verb-Noun, parameters, Pester, Az module | `.context/standards/powershell.md` | +| Terraform | File layout, modules, tflint, Terratest | `.context/standards/terraform.md` | +| ADO Pipelines | Triggers, templates, environments, approvals | `.context/standards/ado-pipelines.md` | +| Docker | Multi-stage builds, layer optimisation, scanning | `.context/standards/docker.md` | +| Playwright | Page Object Model, semantic locators, auto-waiting, fixtures | `.context/standards/playwright.md` | +| OpenTelemetry | Cross-language SDK patterns, OTLP protocol, backends | `.context/standards/opentelemetry.md` | +| OpenTelemetry .NET | .NET instrumentation, pitfalls, testing patterns | `.context/standards/opentelemetry-dotnet.md` | + +--- + +## Project-Specific Rules [CONFIGURE] + + diff --git a/mcp-server/test/fixtures/repo/playbooks/assess/security.md b/mcp-server/test/fixtures/repo/playbooks/assess/security.md new file mode 100644 index 0000000..d4a8e0e --- /dev/null +++ b/mcp-server/test/fixtures/repo/playbooks/assess/security.md @@ -0,0 +1,15 @@ +--- +name: assess-security +description: "Run comprehensive OWASP Top 10 security assessment (fixture)" +keywords: [assess security, security audit, threat model] +--- + +# Security Assessment (fixture) + +## Role + +Principal Security Engineer. + +## Phase 1: Discovery + +Identify entry points. diff --git a/mcp-server/test/fixtures/repo/playbooks/review/security.md b/mcp-server/test/fixtures/repo/playbooks/review/security.md new file mode 100644 index 0000000..ebfb645 --- /dev/null +++ b/mcp-server/test/fixtures/repo/playbooks/review/security.md @@ -0,0 +1,11 @@ +--- +name: review-security +description: "OWASP, secrets, injection vectors (fixture)" +keywords: [review security, security review] +--- + +# Security Review (fixture) + +## Role + +Security reviewer. diff --git a/mcp-server/test/fixtures/repo/standards/security.md b/mcp-server/test/fixtures/repo/standards/security.md new file mode 100644 index 0000000..3097387 --- /dev/null +++ b/mcp-server/test/fixtures/repo/standards/security.md @@ -0,0 +1,14 @@ +# Secure Coding Standards — OWASP Top 10 & Beyond (fixture) + +## Core Principle + +Every line of code must be written with security as a first-class concern. + +## Non-Negotiables + +1. Never log secrets. +2. Validate all input. + +## Decision Checklist + +- [ ] Have you validated all inputs? diff --git a/mcp-server/test/fixtures/repo/standards/testing.md b/mcp-server/test/fixtures/repo/standards/testing.md new file mode 100644 index 0000000..cc177fb --- /dev/null +++ b/mcp-server/test/fixtures/repo/standards/testing.md @@ -0,0 +1,9 @@ +# Testing Standards (fixture) + +## Test Trophy Model + +Static > integration > unit > e2e. + +## Non-Negotiables + +1. No sleep() in tests. diff --git a/mcp-server/test/helpers/fixture-server.ts b/mcp-server/test/helpers/fixture-server.ts new file mode 100644 index 0000000..cc0bf9a --- /dev/null +++ b/mcp-server/test/helpers/fixture-server.ts @@ -0,0 +1,47 @@ +/** + * Minimal static file HTTP server used by tests to stand in for + * raw.githubusercontent.com (or a team's published fork) without touching + * the network. Serves files from a fixture directory and tracks how many + * times each path was requested, so tests can assert on caching behaviour. + */ + +import { createServer, type Server } from "node:http"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { AddressInfo } from "node:net"; + +export interface FixtureServer { + url: string; + requestCounts: Record; + close(): Promise; +} + +export function startFixtureServer(rootDir: string): Promise { + const requestCounts: Record = {}; + let server: Server; + + return new Promise((resolvePromise, reject) => { + server = createServer(async (req, res) => { + const path = decodeURIComponent(req.url || "/").replace(/^\/+/, ""); + requestCounts[path] = (requestCounts[path] || 0) + 1; + try { + const data = await readFile(join(rootDir, path)); + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end(data); + } catch { + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("Not Found"); + } + }); + + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address() as AddressInfo; + resolvePromise({ + url: `http://127.0.0.1:${address.port}`, + requestCounts, + close: () => new Promise((res) => server.close(() => res())), + }); + }); + }); +} diff --git a/mcp-server/test/init.test.ts b/mcp-server/test/init.test.ts new file mode 100644 index 0000000..62df8de --- /dev/null +++ b/mcp-server/test/init.test.ts @@ -0,0 +1,343 @@ +import { test, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm, writeFile, mkdir } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runInit } from "../src/init.js"; +import { startFixtureServer, type FixtureServer } from "./helpers/fixture-server.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = join(__dirname, "fixtures", "repo"); + +let fixtureServer: FixtureServer; + +before(async () => { + fixtureServer = await startFixtureServer(FIXTURES_DIR); +}); + +after(async () => { + await fixtureServer.close(); +}); + +async function withTempDir(fn: (dir: string) => Promise): Promise { + const dir = await mkdtemp(join(tmpdir(), "agentic-context-init-test-")); + try { + await fn(dir); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** Capture console.error output for the duration of fn, then restore it. */ +async function captureStderr(fn: () => Promise): Promise { + const logs: string[] = []; + const original = console.error; + console.error = (...args: unknown[]) => { + logs.push(args.map(String).join(" ")); + }; + try { + await fn(); + } finally { + console.error = original; + } + return logs; +} + +/** + * Stub `global.fetch` to answer Azure DevOps Git Items API requests from the + * local fixture directory instead of hitting dev.azure.com, so the + * azure-devops init tests stay offline and deterministic. Also asserts every + * request carries Basic auth with the expected PAT. + */ +async function withStubbedAzureDevOpsFetch(expectedPat: string, fn: () => Promise): Promise { + const original = globalThis.fetch; + globalThis.fetch = (async (url: string, init?: RequestInit) => { + const parsed = new URL(url); + assert.equal(parsed.hostname, "dev.azure.com"); + + const headers = new Headers(init?.headers); + const expectedAuth = `Basic ${Buffer.from(`:${expectedPat}`).toString("base64")}`; + assert.equal(headers.get("authorization"), expectedAuth); + + const relativePath = parsed.searchParams.get("path")!.replace(/^\/+/, ""); + try { + const text = await readFile(join(FIXTURES_DIR, relativePath), "utf-8"); + return new Response(text, { status: 200 }); + } catch { + return new Response("Not found", { status: 404 }); + } + }) as typeof fetch; + + try { + await fn(); + } finally { + globalThis.fetch = original; + } +} + +test("writes an MCP-flavoured AGENTS.md, replacing the file-based Context System section", async () => { + await withTempDir(async (dir) => { + await captureStderr(() => + runInit([dir, "--agents", "claude", "--content-base-url", fixtureServer.url]) + ); + + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + assert.match(agentsMd, /agentic-context` MCP server for on-demand access/); + assert.match(agentsMd, /Call the `search` tool/); + assert.doesNotMatch(agentsMd, /read `\.context\/index\.md` and load files/); + // The rest of the template (mandated standards tables etc.) should survive untouched + assert.match(agentsMd, /## Mandated Standards/); + assert.match(agentsMd, /Simplicity First/); + }); +}); + +test("writes claude redirect file and registers the MCP server in .mcp.json", async () => { + await withTempDir(async (dir) => { + await captureStderr(() => + runInit([dir, "--agents", "claude", "--content-base-url", fixtureServer.url]) + ); + + const claudeMd = await readFile(join(dir, "CLAUDE.md"), "utf-8"); + assert.match(claudeMd, /agentic-context` MCP server/); + + const mcpConfig = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8")); + const entry = mcpConfig.mcpServers["agentic-context"]; + assert.equal(entry.command, "npx"); + assert.deepEqual(entry.args, ["-y", "agentic-context-mcp"]); + }); +}); + +test("propagates --content-base-url into the written server's CONTENT_BASE_URL env", async () => { + await withTempDir(async (dir) => { + await captureStderr(() => + runInit([dir, "--agents", "cursor", "--content-base-url", fixtureServer.url]) + ); + + const mcpConfig = JSON.parse(await readFile(join(dir, ".cursor", "mcp.json"), "utf-8")); + assert.equal(mcpConfig.mcpServers["agentic-context"].env.CONTENT_BASE_URL, fixtureServer.url); + }); +}); + +test("omits the env override when --content-base-url is not provided", { timeout: 20000 }, async () => { + // No --content-base-url here: this exercises the real default (upstream + // GitHub) content source, so it needs network access, unlike the other + // init tests which stay offline via the fixture server. + await withTempDir(async (dir) => { + await captureStderr(() => runInit([dir, "--agents", "cursor"])); + + const mcpConfig = JSON.parse(await readFile(join(dir, ".cursor", "mcp.json"), "utf-8")); + const entry = mcpConfig.mcpServers["agentic-context"]; + assert.equal(entry.command, "npx"); + assert.equal(entry.env, undefined); + }); +}); + +test("writes .vscode/mcp.json under the 'servers' key for copilot", async () => { + await withTempDir(async (dir) => { + await captureStderr(() => + runInit([dir, "--agents", "copilot", "--content-base-url", fixtureServer.url]) + ); + + const mcpConfig = JSON.parse(await readFile(join(dir, ".vscode", "mcp.json"), "utf-8")); + assert.ok(mcpConfig.servers["agentic-context"]); + assert.equal(mcpConfig.mcpServers, undefined); + }); +}); + +test("windsurf prints the config snippet instead of writing a project file", async () => { + await withTempDir(async (dir) => { + const logs = await captureStderr(() => + runInit([dir, "--agents", "windsurf", "--content-base-url", fixtureServer.url]) + ); + + assert.ok(logs.some((l) => l.includes("~/.codeium/windsurf/mcp_config.json"))); + assert.ok(logs.some((l) => l.includes("agentic-context"))); + }); +}); + +test("--no-overwrite skips files that already exist", async () => { + await withTempDir(async (dir) => { + const sentinel = "# SENTINEL — do not overwrite\n"; + await writeFile(join(dir, "AGENTS.md"), sentinel, "utf-8"); + + await captureStderr(() => + runInit([dir, "--agents", "claude", "--content-base-url", fixtureServer.url, "--no-overwrite"]) + ); + + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + assert.equal(agentsMd, sentinel); + }); +}); + +test("overwrites by default when a file already exists", async () => { + await withTempDir(async (dir) => { + await writeFile(join(dir, "AGENTS.md"), "# stale content\n", "utf-8"); + + await captureStderr(() => + runInit([dir, "--agents", "claude", "--content-base-url", fixtureServer.url]) + ); + + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + assert.match(agentsMd, /agentic-context` MCP server/); + }); +}); + +test("merging MCP config preserves unrelated pre-existing servers", async () => { + await withTempDir(async (dir) => { + await mkdir(dir, { recursive: true }); + await writeFile( + join(dir, ".mcp.json"), + JSON.stringify({ mcpServers: { "some-other-server": { command: "foo", args: ["bar"] } } }), + "utf-8" + ); + + await captureStderr(() => + runInit([dir, "--agents", "claude", "--content-base-url", fixtureServer.url]) + ); + + const mcpConfig = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8")); + assert.deepEqual(mcpConfig.mcpServers["some-other-server"], { command: "foo", args: ["bar"] }); + assert.ok(mcpConfig.mcpServers["agentic-context"]); + }); +}); + +test("running with --agents all configures every agent without throwing", async () => { + await withTempDir(async (dir) => { + await captureStderr(() => + runInit([dir, "--agents", "all", "--content-base-url", fixtureServer.url]) + ); + + for (const file of [ + "AGENTS.md", + "CLAUDE.md", + join(".cursor", "rules", "standards.mdc"), + ".windsurfrules", + join(".github", "copilot-instructions.md"), + join(".devin", "devin.json"), + ".mcp.json", + join(".cursor", "mcp.json"), + join(".vscode", "mcp.json"), + join(".devin", "mcp_config.json"), + ]) { + await assert.doesNotReject( + readFile(join(dir, file), "utf-8"), + `expected ${file} to be written` + ); + } + }); +}); + +test("defaults to all agents when --agents is omitted", async () => { + await withTempDir(async (dir) => { + await captureStderr(() => runInit([dir, "--content-base-url", fixtureServer.url])); + await assert.doesNotReject(readFile(join(dir, "CLAUDE.md"), "utf-8")); + await assert.doesNotReject(readFile(join(dir, ".windsurfrules"), "utf-8")); + }); +}); + +test("azure-devops source: fetches AGENTS.md via the Git Items API and authenticates with the PAT", async () => { + process.env.AZURE_DEVOPS_PAT = "test-pat"; + try { + await withStubbedAzureDevOpsFetch("test-pat", () => + withTempDir(async (dir) => { + await captureStderr(() => + runInit([ + dir, + "--agents", + "claude", + "--source-type", + "azure-devops", + "--azure-org", + "my-org", + "--azure-project", + "my-project", + "--azure-repo", + "agentic-context", + ]) + ); + + const agentsMd = await readFile(join(dir, "AGENTS.md"), "utf-8"); + assert.match(agentsMd, /agentic-context` MCP server for on-demand access/); + assert.match(agentsMd, /## Mandated Standards/); + }) + ); + } finally { + delete process.env.AZURE_DEVOPS_PAT; + } +}); + +test("azure-devops source: writes a file-reference PAT and credentials instructions, never the real PAT", async () => { + process.env.AZURE_DEVOPS_PAT = "super-secret-pat"; + try { + await withStubbedAzureDevOpsFetch("super-secret-pat", () => + withTempDir(async (dir) => { + await captureStderr(() => + runInit([ + dir, + "--agents", + "claude", + "--source-type", + "azure-devops", + "--azure-org", + "my-org", + "--azure-project", + "my-project", + "--azure-repo", + "agentic-context", + "--azure-branch", + "release", + ]) + ); + + const mcpConfig = JSON.parse(await readFile(join(dir, ".mcp.json"), "utf-8")); + const env = mcpConfig.mcpServers["agentic-context"].env; + assert.equal(env.CONTENT_SOURCE_TYPE, "azure-devops"); + assert.equal(env.AZURE_DEVOPS_ORG, "my-org"); + assert.equal(env.AZURE_DEVOPS_PROJECT, "my-project"); + assert.equal(env.AZURE_DEVOPS_REPO, "agentic-context"); + assert.equal(env.AZURE_DEVOPS_BRANCH, "release"); + assert.equal(env.AZURE_DEVOPS_PAT, "${file:.devin/credentials/agentic-context}"); + assert.ok(!JSON.stringify(mcpConfig).includes("super-secret-pat")); + + const credentials = await readFile(join(dir, ".devin", "credentials", "agentic-context"), "utf-8"); + assert.match(credentials, /Code \(Read\) scope/); + + const gitignore = await readFile(join(dir, ".gitignore"), "utf-8"); + assert.ok(gitignore.includes(".devin/credentials/")); + }) + ); + } finally { + delete process.env.AZURE_DEVOPS_PAT; + } +}); + +test("azure-devops source: prints a reminder to fill in the credentials file", async () => { + process.env.AZURE_DEVOPS_PAT = "test-pat"; + try { + await withStubbedAzureDevOpsFetch("test-pat", () => + withTempDir(async (dir) => { + const logs = await captureStderr(() => + runInit([ + dir, + "--agents", + "claude", + "--source-type", + "azure-devops", + "--azure-org", + "my-org", + "--azure-project", + "my-project", + "--azure-repo", + "agentic-context", + ]) + ); + + assert.ok(logs.some((l) => l.includes(".devin/credentials/agentic-context"))); + assert.ok(logs.some((l) => l.includes("gitignored"))); + }) + ); + } finally { + delete process.env.AZURE_DEVOPS_PAT; + } +}); diff --git a/mcp-server/tsconfig.json b/mcp-server/tsconfig.json new file mode 100644 index 0000000..b47573a --- /dev/null +++ b/mcp-server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "build"] +}