From f168d5be6164abc4c9b74f731c18558cc70d4434 Mon Sep 17 00:00:00 2001 From: Jeremy Gayed Date: Mon, 9 Feb 2026 13:31:43 -0500 Subject: [PATCH] feat: introduce local mcp-server for interacting with the CLI --- packages/mcp-server/README.md | 199 +++++++++ packages/mcp-server/bin/mcp-server.js | 2 + packages/mcp-server/package.json | 68 +++ packages/mcp-server/project.json | 46 +++ packages/mcp-server/src/config.test.ts | 156 +++++++ packages/mcp-server/src/config.ts | 17 + packages/mcp-server/src/index.ts | 16 + packages/mcp-server/src/subprocess.test.ts | 222 ++++++++++ packages/mcp-server/src/subprocess.ts | 117 ++++++ .../mcp-server/src/tools/auth-login.test.ts | 74 ++++ packages/mcp-server/src/tools/auth-login.ts | 25 ++ .../mcp-server/src/tools/cli-help.test.ts | 76 ++++ packages/mcp-server/src/tools/cli-help.ts | 44 ++ packages/mcp-server/src/tools/cli-run.test.ts | 97 +++++ packages/mcp-server/src/tools/cli-run.ts | 42 ++ packages/mcp-server/src/tools/index.ts | 11 + packages/mcp-server/tsconfig.build.json | 4 + packages/mcp-server/tsconfig.json | 10 + packages/mcp-server/vite.config.ts | 3 + pnpm-lock.yaml | 387 +++++++++++++++++- 20 files changed, 1612 insertions(+), 4 deletions(-) create mode 100644 packages/mcp-server/README.md create mode 100644 packages/mcp-server/bin/mcp-server.js create mode 100644 packages/mcp-server/package.json create mode 100644 packages/mcp-server/project.json create mode 100644 packages/mcp-server/src/config.test.ts create mode 100644 packages/mcp-server/src/config.ts create mode 100644 packages/mcp-server/src/index.ts create mode 100644 packages/mcp-server/src/subprocess.test.ts create mode 100644 packages/mcp-server/src/subprocess.ts create mode 100644 packages/mcp-server/src/tools/auth-login.test.ts create mode 100644 packages/mcp-server/src/tools/auth-login.ts create mode 100644 packages/mcp-server/src/tools/cli-help.test.ts create mode 100644 packages/mcp-server/src/tools/cli-help.ts create mode 100644 packages/mcp-server/src/tools/cli-run.test.ts create mode 100644 packages/mcp-server/src/tools/cli-run.ts create mode 100644 packages/mcp-server/src/tools/index.ts create mode 100644 packages/mcp-server/tsconfig.build.json create mode 100644 packages/mcp-server/tsconfig.json create mode 100644 packages/mcp-server/vite.config.ts diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md new file mode 100644 index 00000000000..23f427e576a --- /dev/null +++ b/packages/mcp-server/README.md @@ -0,0 +1,199 @@ +# @shopify/mcp-server + +An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that lets AI assistants interact with the [Shopify CLI](https://shopify.dev/docs/api/shopify-cli). Works with any MCP client — Claude Code, Claude Desktop, Cursor, Windsurf, and others. + +The server wraps the `shopify` CLI binary as a subprocess. It doesn't depend on CLI internals, so it works with any installed version of Shopify CLI. + +## Prerequisites + +- [Node.js](https://nodejs.org/) >= 20.10 +- [Shopify CLI](https://shopify.dev/docs/api/shopify-cli) installed and available on your `PATH`: + +```sh +npm install -g @shopify/cli +shopify version +``` + +## Setup + +### Claude Code + +```sh +claude mcp add shopify-cli -- npx @shopify/mcp-server +``` + +To set a default store: + +```sh +claude mcp add shopify-cli -e SHOPIFY_FLAG_STORE=my-store.myshopify.com -- npx @shopify/mcp-server +``` + +### Claude Desktop + +Add to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "shopify-cli": { + "command": "npx", + "args": ["@shopify/mcp-server"], + "env": { + "SHOPIFY_FLAG_STORE": "my-store.myshopify.com" + } + } + } +} +``` + +### Cursor / Windsurf / Other MCP Clients + +Add to your MCP configuration (exact location varies by client): + +```json +{ + "mcpServers": { + "shopify-cli": { + "command": "npx", + "args": ["@shopify/mcp-server"] + } + } +} +``` + +## Tools + +The server exposes three tools: + +### `shopify_cli_run` + +Run any Shopify CLI command. + +``` +shopify_cli_run({ command: "theme list --json" }) +shopify_cli_run({ command: "theme push --force --json" }) +shopify_cli_run({ command: "app deploy --force" }) +``` + +The server automatically: + +- Appends `--no-color` to all commands +- Injects store, auth token, and path from environment config +- Detects authentication errors and suggests using `shopify_auth_login` +- Auto-detects and pretty-prints JSON output + +### `shopify_cli_help` + +Get help for any CLI command. Returns the CLI's own help text plus tips for non-interactive usage. + +``` +shopify_cli_help({}) # All top-level commands +shopify_cli_help({ command: "theme" }) # All theme subcommands +shopify_cli_help({ command: "theme push" }) # Flags for theme push +shopify_cli_help({ command: "app deploy" }) # Flags for app deploy +``` + +### `shopify_auth_login` + +Log in to your Shopify account. Opens a browser for OAuth authentication with a 5-minute timeout. + +``` +shopify_auth_login({}) # Login (uses configured store) +shopify_auth_login({ store: "my-store.myshopify.com" }) # Login to a specific store +``` + +## Authentication + +The Shopify CLI caches OAuth sessions to disk. Once you log in, subsequent commands reuse the cached session automatically — no token management needed. + +**First-time setup:** When you (or the AI) run a command before logging in, the server detects the auth error and suggests using the `shopify_auth_login` tool, which opens your browser for OAuth. + +**Headless / CI environments:** Set `SHOPIFY_CLI_THEME_TOKEN` to a token from the [Theme Access app](https://shopify.dev/docs/themes/tools/theme-access) or an Admin API token to skip browser-based login entirely. + +## Configuration + +All configuration is through environment variables. Set them in your MCP client config's `env` block. + +| Variable | Description | Default | +|----------|-------------|---------| +| `SHOPIFY_FLAG_STORE` | Default store URL (e.g. `my-store.myshopify.com`) | — | +| `SHOPIFY_CLI_THEME_TOKEN` | Theme Access / Admin API token for headless auth | — | +| `SHOPIFY_FLAG_PATH` | Default project directory | — | +| `SHOPIFY_CLI_PATH` | Path to the `shopify` binary | `shopify` | +| `SHOPIFY_MCP_TIMEOUT` | Command timeout in milliseconds | `120000` | + +These are injected as environment variables into the CLI subprocess, where the CLI reads them as default values. Flags passed explicitly in a command (e.g. `--store other-store.myshopify.com`) take precedence. + +## Examples + +**List themes in a store:** +> "Show me all the themes in my store" +> +> → `shopify_cli_run({ command: "theme list --json" })` + +**Push theme changes:** +> "Push my local theme changes" +> +> → `shopify_cli_run({ command: "theme push --force --json" })` + +**Check theme quality:** +> "Run theme check on my code" +> +> → `shopify_cli_run({ command: "theme check --output json" })` + +**Deploy an app:** +> "Deploy my app to production" +> +> → `shopify_cli_run({ command: "app deploy --force" })` + +**Discover available commands:** +> "What app commands are available?" +> +> → `shopify_cli_help({ command: "app" })` + +**Log in to a store:** +> "Log in to my-store.myshopify.com" +> +> → `shopify_auth_login({ store: "my-store.myshopify.com" })` + +## Development + +```sh +# From the monorepo root +pnpm install +cd packages/mcp-server + +# Run tests +pnpm vitest run + +# Build +pnpm build + +# Type-check +pnpm type-check + +# Lint +pnpm lint + +# Test the server manually +echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}},"id":1}' | node bin/mcp-server.js +``` + +## Security + +This MCP server can execute any Shopify CLI command on behalf of the connected AI agent. By installing and configuring it, you trust the MCP client to run CLI commands with your authenticated session. + +**What's safe by design:** + +- The CLI requires authentication (OAuth or token) — the MCP server cannot bypass this +- `execa` does not use a shell, preventing shell injection +- Long-running commands (`theme dev`, `app dev`) are detected and blocked with a helpful message + +**What to be aware of:** + +- Destructive commands (`theme delete`, `theme publish`, `app deploy`) can be executed if the agent passes `--force`. +- If using `SHOPIFY_CLI_THEME_TOKEN`, scope the token appropriately (e.g., theme-only access, not full admin). + +## License + +MIT diff --git a/packages/mcp-server/bin/mcp-server.js b/packages/mcp-server/bin/mcp-server.js new file mode 100644 index 00000000000..ac44bdf7504 --- /dev/null +++ b/packages/mcp-server/bin/mcp-server.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import('../dist/index.js') diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json new file mode 100644 index 00000000000..011342aa606 --- /dev/null +++ b/packages/mcp-server/package.json @@ -0,0 +1,68 @@ +{ + "name": "@shopify/mcp-server", + "version": "3.90.0", + "packageManager": "pnpm@10.11.1", + "private": false, + "description": "MCP server for Shopify CLI — lets AI assistants run Shopify theme and app commands", + "bugs": { + "url": "https://community.shopify.dev/c/shopify-cli-libraries/14" + }, + "repository": { + "type": "git", + "url": "https://github.com/Shopify/cli.git", + "directory": "packages/mcp-server" + }, + "license": "MIT", + "author": "Shopify", + "type": "module", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "shopify-mcp-server": "./bin/mcp-server.js" + }, + "files": [ + "/dist", + "/bin" + ], + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "nx build", + "clean": "nx clean", + "lint": "nx lint", + "lint:fix": "nx lint:fix", + "prepack": "NODE_ENV=production pnpm nx build", + "vitest": "vitest", + "type-check": "nx type-check" + }, + "eslintConfig": { + "extends": [ + "../../.eslintrc.cjs" + ] + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "execa": "^7.2.0", + "zod": "^3.25.0" + }, + "devDependencies": { + "@vitest/coverage-istanbul": "^3.1.4" + }, + "engines": { + "node": ">=20.10.0" + }, + "os": [ + "darwin", + "linux", + "win32" + ], + "publishConfig": { + "@shopify:registry": "https://registry.npmjs.org", + "access": "public" + }, + "engine-strict": true +} diff --git a/packages/mcp-server/project.json b/packages/mcp-server/project.json new file mode 100644 index 00000000000..8aa0af1c259 --- /dev/null +++ b/packages/mcp-server/project.json @@ -0,0 +1,46 @@ +{ + "name": "mcp-server", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/mcp-server/src", + "projectType": "library", + "tags": ["scope:plugin"], + "targets": { + "clean": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm rimraf dist/", + "cwd": "packages/mcp-server" + } + }, + "build": { + "executor": "nx:run-commands", + "outputs": ["{workspaceRoot}/dist"], + "inputs": ["{projectRoot}/src/**/*", "{projectRoot}/package.json"], + "options": { + "command": "pnpm tsc -b ./tsconfig.build.json", + "cwd": "packages/mcp-server" + } + }, + "lint": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm eslint \"src/**/*.ts\"", + "cwd": "packages/mcp-server" + } + }, + "lint:fix": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm eslint 'src/**/*.ts' --fix", + "cwd": "packages/mcp-server" + } + }, + "type-check": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm tsc --noEmit", + "cwd": "packages/mcp-server" + } + } + } +} diff --git a/packages/mcp-server/src/config.test.ts b/packages/mcp-server/src/config.test.ts new file mode 100644 index 00000000000..994aa4a1e5b --- /dev/null +++ b/packages/mcp-server/src/config.test.ts @@ -0,0 +1,156 @@ +import {resolveConfig} from './config.js' +import {describe, expect, test, beforeEach, afterEach} from 'vitest' + +describe('resolveConfig', () => { + const originalEnv = process.env + + beforeEach(() => { + process.env = {...originalEnv} + delete process.env.SHOPIFY_CLI_PATH + delete process.env.SHOPIFY_FLAG_STORE + delete process.env.SHOPIFY_CLI_THEME_TOKEN + delete process.env.SHOPIFY_FLAG_PATH + delete process.env.SHOPIFY_MCP_TIMEOUT + }) + + afterEach(() => { + process.env = originalEnv + }) + + test('returns defaults when no env vars or overrides are set', () => { + // Given + // no env vars, no overrides + + // When + const config = resolveConfig() + + // Then + expect(config.shopifyCliPath).toBe('shopify') + expect(config.store).toBeUndefined() + expect(config.themeAccessPassword).toBeUndefined() + expect(config.path).toBeUndefined() + expect(config.timeout).toBe(120_000) + }) + + test('reads shopifyCliPath from SHOPIFY_CLI_PATH env var', () => { + // Given + process.env.SHOPIFY_CLI_PATH = '/usr/local/bin/shopify' + + // When + const config = resolveConfig() + + // Then + expect(config.shopifyCliPath).toBe('/usr/local/bin/shopify') + }) + + test('reads store from SHOPIFY_FLAG_STORE env var', () => { + // Given + process.env.SHOPIFY_FLAG_STORE = 'my-store.myshopify.com' + + // When + const config = resolveConfig() + + // Then + expect(config.store).toBe('my-store.myshopify.com') + }) + + test('reads themeAccessPassword from SHOPIFY_CLI_THEME_TOKEN env var', () => { + // Given + process.env.SHOPIFY_CLI_THEME_TOKEN = 'shptka_secret123' + + // When + const config = resolveConfig() + + // Then + expect(config.themeAccessPassword).toBe('shptka_secret123') + }) + + test('reads path from SHOPIFY_FLAG_PATH env var', () => { + // Given + process.env.SHOPIFY_FLAG_PATH = '/home/user/my-theme' + + // When + const config = resolveConfig() + + // Then + expect(config.path).toBe('/home/user/my-theme') + }) + + test('reads timeout from SHOPIFY_MCP_TIMEOUT env var', () => { + // Given + process.env.SHOPIFY_MCP_TIMEOUT = '60000' + + // When + const config = resolveConfig() + + // Then + expect(config.timeout).toBe(60000) + }) + + test('falls back to default timeout when SHOPIFY_MCP_TIMEOUT is not a valid number', () => { + // Given + process.env.SHOPIFY_MCP_TIMEOUT = 'not-a-number' + + // When + const config = resolveConfig() + + // Then + expect(config.timeout).toBe(120_000) + }) + + test('overrides take precedence over env vars', () => { + // Given + process.env.SHOPIFY_CLI_PATH = '/env/shopify' + process.env.SHOPIFY_FLAG_STORE = 'env-store.myshopify.com' + process.env.SHOPIFY_CLI_THEME_TOKEN = 'env-token' + process.env.SHOPIFY_FLAG_PATH = '/env/path' + process.env.SHOPIFY_MCP_TIMEOUT = '30000' + + // When + const config = resolveConfig({ + shopifyCliPath: '/override/shopify', + store: 'override-store.myshopify.com', + themeAccessPassword: 'override-token', + path: '/override/path', + timeout: 5000, + }) + + // Then + expect(config.shopifyCliPath).toBe('/override/shopify') + expect(config.store).toBe('override-store.myshopify.com') + expect(config.themeAccessPassword).toBe('override-token') + expect(config.path).toBe('/override/path') + expect(config.timeout).toBe(5000) + }) + + test('overrides take precedence over defaults when no env vars are set', () => { + // Given + // no env vars + + // When + const config = resolveConfig({ + shopifyCliPath: '/custom/shopify', + timeout: 10000, + }) + + // Then + expect(config.shopifyCliPath).toBe('/custom/shopify') + expect(config.timeout).toBe(10000) + }) + + test('partial overrides merge with env vars and defaults', () => { + // Given + process.env.SHOPIFY_FLAG_STORE = 'env-store.myshopify.com' + + // When + const config = resolveConfig({ + path: '/override/path', + }) + + // Then + expect(config.shopifyCliPath).toBe('shopify') + expect(config.store).toBe('env-store.myshopify.com') + expect(config.path).toBe('/override/path') + expect(config.timeout).toBe(120_000) + }) +}) diff --git a/packages/mcp-server/src/config.ts b/packages/mcp-server/src/config.ts new file mode 100644 index 00000000000..a6bc74a7542 --- /dev/null +++ b/packages/mcp-server/src/config.ts @@ -0,0 +1,17 @@ +export interface McpServerConfig { + shopifyCliPath: string + store?: string + themeAccessPassword?: string + path?: string + timeout: number +} + +export function resolveConfig(overrides?: Partial): McpServerConfig { + return { + shopifyCliPath: overrides?.shopifyCliPath ?? process.env.SHOPIFY_CLI_PATH ?? 'shopify', + store: overrides?.store ?? process.env.SHOPIFY_FLAG_STORE, + themeAccessPassword: overrides?.themeAccessPassword ?? process.env.SHOPIFY_CLI_THEME_TOKEN, + path: overrides?.path ?? process.env.SHOPIFY_FLAG_PATH, + timeout: overrides?.timeout ?? (Number(process.env.SHOPIFY_MCP_TIMEOUT) || 120_000), + } +} diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts new file mode 100644 index 00000000000..4cb3f888826 --- /dev/null +++ b/packages/mcp-server/src/index.ts @@ -0,0 +1,16 @@ +import {resolveConfig} from './config.js' +import {registerAllTools} from './tools/index.js' +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' +import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js' +import {createRequire} from 'node:module' + +const require = createRequire(import.meta.url) +const {version} = require('../package.json') as {version: string} + +const config = resolveConfig() +const server = new McpServer({name: 'shopify-cli', version}) + +registerAllTools(server, config) + +const transport = new StdioServerTransport() +await server.connect(transport) diff --git a/packages/mcp-server/src/subprocess.test.ts b/packages/mcp-server/src/subprocess.test.ts new file mode 100644 index 00000000000..f592cdf33fe --- /dev/null +++ b/packages/mcp-server/src/subprocess.test.ts @@ -0,0 +1,222 @@ +import {buildEnv, isAuthError, formatToolResult, execShopify, isLongRunningCommand} from './subprocess.js' +import {describe, expect, test, vi} from 'vitest' +import type {McpServerConfig} from './config.js' +import type {ExecResult} from './subprocess.js' + +vi.mock('execa', () => ({ + execa: vi.fn(), +})) + +describe('execShopify', () => { + test('calls execa with args plus --no-color appended', async () => { + // Given + const {execa: mockExeca} = await import('execa') + vi.mocked(mockExeca).mockResolvedValue({ + stdout: '{}', + stderr: '', + exitCode: 0, + } as any) + + const config: McpServerConfig = { + shopifyCliPath: '/usr/local/bin/shopify', + store: 'my-store.myshopify.com', + themeAccessPassword: 'token123', + timeout: 60000, + } + + // When + await execShopify(config, ['theme', 'list', '--json']) + + // Then + expect(mockExeca).toHaveBeenCalledWith( + '/usr/local/bin/shopify', + ['theme', 'list', '--json', '--no-color'], + expect.objectContaining({ + reject: false, + timeout: 60000, + env: { + NO_COLOR: '1', + SHOPIFY_FLAG_STORE: 'my-store.myshopify.com', + SHOPIFY_CLI_THEME_TOKEN: 'token123', + }, + }), + ) + }) + + test('uses option timeout over config timeout', async () => { + // Given + const {execa: mockExeca} = await import('execa') + vi.mocked(mockExeca).mockResolvedValue({stdout: '', stderr: '', exitCode: 0} as any) + const config: McpServerConfig = {shopifyCliPath: 'shopify', timeout: 120_000} + + // When + await execShopify(config, ['auth', 'login'], {timeout: 300_000}) + + // Then + expect(mockExeca).toHaveBeenCalledWith('shopify', expect.any(Array), expect.objectContaining({timeout: 300_000})) + }) + + test('returns friendly error when CLI binary is not found', async () => { + // Given + const {execa: mockExeca} = await import('execa') + const enoent = new Error('spawn shopify ENOENT') as Error & {code: string} + enoent.code = 'ENOENT' + vi.mocked(mockExeca).mockRejectedValue(enoent) + const config: McpServerConfig = {shopifyCliPath: 'shopify', timeout: 120_000} + + // When + const result = await execShopify(config, ['theme', 'list']) + + // Then + expect(result.exitCode).toBe(1) + expect(result.stderr).toContain('Shopify CLI not found') + expect(result.stderr).toContain('npm install -g @shopify/cli') + }) + + test('returns stdout, stderr, and exitCode', async () => { + // Given + const {execa: mockExeca} = await import('execa') + vi.mocked(mockExeca).mockResolvedValue({ + stdout: 'output', + stderr: 'warning', + exitCode: 1, + } as any) + const config: McpServerConfig = {shopifyCliPath: 'shopify', timeout: 120_000} + + // When + const result = await execShopify(config, ['theme', 'push']) + + // Then + expect(result).toEqual({stdout: 'output', stderr: 'warning', exitCode: 1}) + }) +}) + +describe('isLongRunningCommand', () => { + test('detects "theme dev" as long-running', () => { + expect(isLongRunningCommand(['theme', 'dev'])).toBe('theme dev') + expect(isLongRunningCommand(['theme', 'dev', '--store', 'x'])).toBe('theme dev') + }) + + test('detects "app dev" as long-running', () => { + expect(isLongRunningCommand(['app', 'dev'])).toBe('app dev') + }) + + test('returns undefined for normal commands', () => { + expect(isLongRunningCommand(['theme', 'list'])).toBeUndefined() + expect(isLongRunningCommand(['theme', 'push'])).toBeUndefined() + expect(isLongRunningCommand(['app', 'deploy'])).toBeUndefined() + }) +}) + +describe('buildEnv', () => { + test('always sets NO_COLOR', () => { + const config: McpServerConfig = {shopifyCliPath: 'shopify', timeout: 120_000} + expect(buildEnv(config)).toEqual({NO_COLOR: '1'}) + }) + + test('sets SHOPIFY_FLAG_STORE when store is configured', () => { + const config: McpServerConfig = {shopifyCliPath: 'shopify', store: 'my-store.myshopify.com', timeout: 120_000} + expect(buildEnv(config).SHOPIFY_FLAG_STORE).toBe('my-store.myshopify.com') + }) + + test('sets SHOPIFY_CLI_THEME_TOKEN when themeAccessPassword is configured', () => { + const config: McpServerConfig = {shopifyCliPath: 'shopify', themeAccessPassword: 'tok', timeout: 120_000} + expect(buildEnv(config).SHOPIFY_CLI_THEME_TOKEN).toBe('tok') + }) + + test('sets SHOPIFY_FLAG_PATH when path is configured', () => { + const config: McpServerConfig = {shopifyCliPath: 'shopify', path: '/themes/dawn', timeout: 120_000} + expect(buildEnv(config).SHOPIFY_FLAG_PATH).toBe('/themes/dawn') + }) + + test('sets all env vars when fully configured', () => { + const config: McpServerConfig = { + shopifyCliPath: 'shopify', + store: 'store', + themeAccessPassword: 'pw', + path: '/p', + timeout: 120_000, + } + const env = buildEnv(config) + expect(env).toEqual({ + NO_COLOR: '1', + SHOPIFY_FLAG_STORE: 'store', + SHOPIFY_CLI_THEME_TOKEN: 'pw', + SHOPIFY_FLAG_PATH: '/p', + }) + }) +}) + +describe('isAuthError', () => { + test.each([ + 'You are not logged in', + 'Authentication required', + 'Run shopify auth login first', + 'Your session has expired', + 'Invalid credentials provided', + 'Please login to continue', + 'Not authenticated', + ])('detects "%s" as auth error', (message) => { + expect(isAuthError(message)).toBe(true) + }) + + test('returns false for non-auth errors', () => { + expect(isAuthError('Theme not found')).toBe(false) + expect(isAuthError('')).toBe(false) + }) +}) + +describe('formatToolResult', () => { + test('returns auth error when exit code is non-zero and output matches auth pattern', () => { + const result: ExecResult = {stdout: '', stderr: 'You are not logged in', exitCode: 1} + const formatted = formatToolResult(result) + expect(formatted.isError).toBe(true) + expect(formatted.content[0]!.text).toContain('shopify_auth_login') + }) + + test('returns generic error when exit code is non-zero', () => { + const result: ExecResult = {stdout: '', stderr: 'Theme not found', exitCode: 1} + const formatted = formatToolResult(result) + expect(formatted.isError).toBe(true) + expect(formatted.content[0]!.text).toBe('Theme not found') + }) + + test('returns exit code message when both stdout and stderr are empty', () => { + const result: ExecResult = {stdout: '', stderr: '', exitCode: 2} + const formatted = formatToolResult(result) + expect(formatted.isError).toBe(true) + expect(formatted.content[0]!.text).toContain('exit code 2') + }) + + test('returns success message when stdout is empty', () => { + const result: ExecResult = {stdout: '', stderr: '', exitCode: 0} + expect(formatToolResult(result).content[0]!.text).toBe('Command completed successfully.') + }) + + test('auto-detects and pretty-prints JSON objects', () => { + const result: ExecResult = {stdout: '{"id":1,"name":"Dawn"}', stderr: '', exitCode: 0} + const formatted = formatToolResult(result) + expect(formatted.content[0]!.text).toBe('{\n "id": 1,\n "name": "Dawn"\n}') + }) + + test('auto-detects and pretty-prints JSON arrays', () => { + const result: ExecResult = {stdout: '[{"id":1}]', stderr: '', exitCode: 0} + const formatted = formatToolResult(result) + expect(formatted.content[0]!.text).toContain('[\n') + }) + + test('returns raw text when output starts with { but is not valid JSON', () => { + const result: ExecResult = {stdout: '{not json', stderr: '', exitCode: 0} + expect(formatToolResult(result).content[0]!.text).toBe('{not json') + }) + + test('returns raw text for non-JSON output', () => { + const result: ExecResult = {stdout: 'Theme pushed successfully', stderr: '', exitCode: 0} + expect(formatToolResult(result).content[0]!.text).toBe('Theme pushed successfully') + }) + + test('trims whitespace from output', () => { + const result: ExecResult = {stdout: ' hello \n', stderr: '', exitCode: 0} + expect(formatToolResult(result).content[0]!.text).toBe('hello') + }) +}) diff --git a/packages/mcp-server/src/subprocess.ts b/packages/mcp-server/src/subprocess.ts new file mode 100644 index 00000000000..4f02157d94d --- /dev/null +++ b/packages/mcp-server/src/subprocess.ts @@ -0,0 +1,117 @@ +import {execa} from 'execa' +import type {McpServerConfig} from './config.js' + +export interface ExecResult { + stdout: string + stderr: string + exitCode: number +} + +const LONG_RUNNING_COMMANDS = ['theme dev', 'app dev'] + +export function isLongRunningCommand(args: string[]): string | undefined { + const command = args.join(' ') + return LONG_RUNNING_COMMANDS.find((lr) => command.startsWith(lr)) +} + +export async function execShopify( + config: McpServerConfig, + args: string[], + options?: {timeout?: number}, +): Promise { + const env = buildEnv(config) + + try { + const result = await execa(config.shopifyCliPath, [...args, '--no-color'], { + reject: false, + timeout: options?.timeout ?? config.timeout, + env, + }) + + return { + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + } + } catch (error: unknown) { + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { + return { + stdout: '', + stderr: `Shopify CLI not found at "${config.shopifyCliPath}". Install it with: npm install -g @shopify/cli`, + exitCode: 1, + } + } + throw error + } +} + +export function buildEnv(config: McpServerConfig): {[key: string]: string} { + const env: {[key: string]: string} = { + NO_COLOR: '1', + } + if (config.store) { + env.SHOPIFY_FLAG_STORE = config.store + } + if (config.themeAccessPassword) { + env.SHOPIFY_CLI_THEME_TOKEN = config.themeAccessPassword + } + if (config.path) { + env.SHOPIFY_FLAG_PATH = config.path + } + return env +} + +const AUTH_ERROR_PATTERNS = [ + 'you are not logged in', + 'authentication required', + 'shopify auth login', + 'session has expired', + 'invalid credentials', + 'login to continue', + 'not authenticated', +] + +export function isAuthError(output: string): boolean { + const lower = output.toLowerCase() + return AUTH_ERROR_PATTERNS.some((pattern) => lower.includes(pattern)) +} + +export function formatToolResult(result: ExecResult): {content: {type: 'text'; text: string}[]; isError?: boolean} { + if (result.exitCode !== 0) { + const errorText = result.stderr || result.stdout + if (isAuthError(errorText)) { + return { + isError: true, + content: [ + { + type: 'text', + text: `Authentication error: ${errorText}\n\nNot authenticated. Use the shopify_auth_login tool to open the browser and log in.`, + }, + ], + } + } + return { + isError: true, + content: [{type: 'text', text: errorText || `Command failed with exit code ${result.exitCode}`}], + } + } + + const output = result.stdout.trim() + if (!output) { + return {content: [{type: 'text', text: 'Command completed successfully.'}]} + } + + if (output.startsWith('{') || output.startsWith('[')) { + try { + const parsed = JSON.parse(output) + return {content: [{type: 'text', text: JSON.stringify(parsed, null, 2)}]} + } catch (error: unknown) { + if (error instanceof SyntaxError) { + return {content: [{type: 'text', text: output}]} + } + throw error + } + } + + return {content: [{type: 'text', text: output}]} +} diff --git a/packages/mcp-server/src/tools/auth-login.test.ts b/packages/mcp-server/src/tools/auth-login.test.ts new file mode 100644 index 00000000000..a9cf33fa123 --- /dev/null +++ b/packages/mcp-server/src/tools/auth-login.test.ts @@ -0,0 +1,74 @@ +import {registerAuthLogin} from './auth-login.js' +import {describe, test, expect, vi, beforeEach} from 'vitest' +import type {McpServerConfig} from '../config.js' + +interface ToolResult { + content: {type: string; text: string}[] + isError?: boolean +} + +const mockExecShopify = vi.fn() +const mockFormatToolResult = vi.fn() + +vi.mock('../subprocess.js', () => ({ + execShopify: (...args: unknown[]) => mockExecShopify(...args), + formatToolResult: (...args: unknown[]) => mockFormatToolResult(...args), +})) + +describe('shopify_auth_login', () => { + const config: McpServerConfig = { + shopifyCliPath: 'shopify', + store: 'default-store.myshopify.com', + timeout: 120_000, + } + + let handler: (params: {[key: string]: unknown}) => Promise + + beforeEach(() => { + const mockServer = {registerTool: vi.fn()} as any + registerAuthLogin(mockServer, config) + handler = mockServer.registerTool.mock.calls[0]![2]! + mockExecShopify.mockResolvedValue({stdout: 'Logged in', stderr: '', exitCode: 0}) + mockFormatToolResult.mockReturnValue({content: [{type: 'text', text: 'ok'}]}) + }) + + test('registers with correct tool name', () => { + const mockServer = {registerTool: vi.fn()} as any + registerAuthLogin(mockServer, config) + expect(mockServer.registerTool.mock.calls[0]![0]).toBe('shopify_auth_login') + }) + + test('passes auth login args with explicit store', async () => { + // When + await handler({store: 'my-store.myshopify.com'}) + + // Then + expect(mockExecShopify).toHaveBeenCalledWith(config, ['auth', 'login', '--store', 'my-store.myshopify.com'], { + timeout: 120_000, + }) + }) + + test('falls back to config store', async () => { + // When + await handler({}) + + // Then + expect(mockExecShopify).toHaveBeenCalledWith(config, ['auth', 'login', '--store', 'default-store.myshopify.com'], { + timeout: 120_000, + }) + }) + + test('omits --store when no store configured', async () => { + // Given + const noStoreConfig: McpServerConfig = {shopifyCliPath: 'shopify', timeout: 120_000} + const mockServer = {registerTool: vi.fn()} as any + registerAuthLogin(mockServer, noStoreConfig) + const noStoreHandler: (params: {[key: string]: unknown}) => Promise = mockServer.registerTool.mock.calls[0]![2] + + // When + await noStoreHandler({}) + + // Then + expect(mockExecShopify).toHaveBeenCalledWith(noStoreConfig, ['auth', 'login'], {timeout: 120_000}) + }) +}) diff --git a/packages/mcp-server/src/tools/auth-login.ts b/packages/mcp-server/src/tools/auth-login.ts new file mode 100644 index 00000000000..58a5cf554dd --- /dev/null +++ b/packages/mcp-server/src/tools/auth-login.ts @@ -0,0 +1,25 @@ +import {execShopify, formatToolResult} from '../subprocess.js' +import {z} from 'zod' +import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' +import type {McpServerConfig} from '../config.js' + +export function registerAuthLogin(server: McpServer, config: McpServerConfig): void { + server.registerTool( + 'shopify_auth_login', + { + description: + 'Log in to your Shopify account. Opens the browser for OAuth authentication. Use this when other commands fail with authentication errors.', + inputSchema: { + store: z.string().optional().describe('Store URL to associate with the login'), + }, + }, + async (params) => { + const args = ['auth', 'login'] + const store = params.store ?? config.store + if (store) args.push('--store', store) + + const result = await execShopify(config, args, {timeout: 120_000}) + return formatToolResult(result) + }, + ) +} diff --git a/packages/mcp-server/src/tools/cli-help.test.ts b/packages/mcp-server/src/tools/cli-help.test.ts new file mode 100644 index 00000000000..eef0e6f89e9 --- /dev/null +++ b/packages/mcp-server/src/tools/cli-help.test.ts @@ -0,0 +1,76 @@ +import {registerCliHelp} from './cli-help.js' +import {describe, test, expect, vi, beforeEach} from 'vitest' +import type {McpServerConfig} from '../config.js' + +interface ToolResult { + content: {type: string; text: string}[] + isError?: boolean +} + +const mockExecShopify = vi.fn() + +vi.mock('../subprocess.js', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + execShopify: (...args: unknown[]) => mockExecShopify(...args), + } +}) + +describe('shopify_cli_help', () => { + const config: McpServerConfig = {shopifyCliPath: 'shopify', timeout: 120_000} + let handler: (params: {[key: string]: unknown}) => Promise + + beforeEach(() => { + const mockServer = {registerTool: vi.fn()} as any + registerCliHelp(mockServer, config) + handler = mockServer.registerTool.mock.calls[0]![2]! + mockExecShopify.mockResolvedValue({stdout: 'Usage: shopify theme ...', stderr: '', exitCode: 0}) + }) + + test('registers with correct tool name', () => { + const mockServer = {registerTool: vi.fn()} as any + registerCliHelp(mockServer, config) + expect(mockServer.registerTool.mock.calls[0]![0]).toBe('shopify_cli_help') + }) + + test('runs "shopify --help" when no command given', async () => { + // When + await handler({}) + + // Then + expect(mockExecShopify).toHaveBeenCalledWith(config, ['--help']) + }) + + test('runs " --help" when command is given', async () => { + // When + await handler({command: 'theme push'}) + + // Then + expect(mockExecShopify).toHaveBeenCalledWith(config, ['theme', 'push', '--help']) + }) + + test('appends MCP tips to output', async () => { + // When + const result = await handler({}) + + // Then + expect(result.content[0]!.text).toContain('Usage: shopify theme ...') + expect(result.content[0]!.text).toContain('MCP Usage Tips') + expect(result.content[0]!.text).toContain('--json') + expect(result.content[0]!.text).toContain('--force') + }) + + test('propagates isError when help command fails', async () => { + // Given + mockExecShopify.mockResolvedValue({stdout: '', stderr: 'Command not found', exitCode: 1}) + + // When + const result = await handler({command: 'nonexistent'}) + + // Then + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('Command not found') + expect(result.content[0]!.text).toContain('MCP Usage Tips') + }) +}) diff --git a/packages/mcp-server/src/tools/cli-help.ts b/packages/mcp-server/src/tools/cli-help.ts new file mode 100644 index 00000000000..63b23942de0 --- /dev/null +++ b/packages/mcp-server/src/tools/cli-help.ts @@ -0,0 +1,44 @@ +import {execShopify, formatToolResult} from '../subprocess.js' +import {z} from 'zod' +import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' +import type {McpServerConfig} from '../config.js' + +const MCP_TIPS = ` +## MCP Usage Tips + +- --no-color is always appended automatically by this server +- Use --json flag when available for structured, parseable output +- Use --force or -f to skip confirmation prompts (required for non-interactive use) +- Store is injected from server config via SHOPIFY_FLAG_STORE if configured +- Theme Access token is injected via SHOPIFY_CLI_THEME_TOKEN if configured +- For theme push/pull: use --only and --ignore to filter files +- theme dev and app dev are long-running processes — start them in a separate terminal, not through this server +- Use the shopify_auth_login tool if you encounter authentication errors`.trim() + +export function registerCliHelp(server: McpServer, config: McpServerConfig): void { + server.registerTool( + 'shopify_cli_help', + { + description: + 'Get help for any Shopify CLI command. Returns usage info, available flags, and MCP-specific tips. Call with no command to see all available commands.', + inputSchema: { + command: z + .string() + .optional() + .describe( + 'CLI command to get help for (e.g. "theme push", "theme", "app deploy", "auth"). Omit to see all top-level commands.', + ), + }, + }, + async (params) => { + const args = params.command ? params.command.split(/\s+/) : [] + args.push('--help') + + const result = await execShopify(config, args) + const formatted = formatToolResult(result) + const helpText = formatted.content[0]?.text ?? '' + + return {content: [{type: 'text', text: `${helpText}\n\n${MCP_TIPS}`}], isError: formatted.isError} + }, + ) +} diff --git a/packages/mcp-server/src/tools/cli-run.test.ts b/packages/mcp-server/src/tools/cli-run.test.ts new file mode 100644 index 00000000000..2ce6895c5f1 --- /dev/null +++ b/packages/mcp-server/src/tools/cli-run.test.ts @@ -0,0 +1,97 @@ +import {registerCliRun} from './cli-run.js' +import {describe, test, expect, vi, beforeEach} from 'vitest' +import type {McpServerConfig} from '../config.js' + +interface ToolResult { + content: {type: string; text: string}[] + isError?: boolean +} + +const mockExecShopify = vi.fn() +const mockFormatToolResult = vi.fn() + +vi.mock('../subprocess.js', () => ({ + execShopify: (...args: unknown[]) => mockExecShopify(...args), + formatToolResult: (...args: unknown[]) => mockFormatToolResult(...args), + isLongRunningCommand: (args: string[]) => { + const cmd = args.join(' ') + if (cmd.startsWith('theme dev')) return 'theme dev' + if (cmd.startsWith('app dev')) return 'app dev' + return undefined + }, +})) + +describe('shopify_cli_run', () => { + const config: McpServerConfig = {shopifyCliPath: 'shopify', store: 'my-store.myshopify.com', timeout: 120_000} + let handler: (params: {[key: string]: unknown}) => Promise + + beforeEach(() => { + const mockServer = {registerTool: vi.fn()} as any + registerCliRun(mockServer, config) + handler = mockServer.registerTool.mock.calls[0]![2]! + mockExecShopify.mockResolvedValue({stdout: '[]', stderr: '', exitCode: 0}) + mockFormatToolResult.mockReturnValue({content: [{type: 'text', text: '[]'}]}) + }) + + test('registers with correct tool name', () => { + const mockServer = {registerTool: vi.fn()} as any + registerCliRun(mockServer, config) + expect(mockServer.registerTool.mock.calls[0]![0]).toBe('shopify_cli_run') + }) + + test('splits command string into args', async () => { + // When + await handler({command: 'theme list --json'}) + + // Then + expect(mockExecShopify).toHaveBeenCalledWith(config, ['theme', 'list', '--json']) + }) + + test('handles command with multiple flags', async () => { + // When + await handler({command: 'theme push --force --json --theme 12345'}) + + // Then + expect(mockExecShopify).toHaveBeenCalledWith(config, ['theme', 'push', '--force', '--json', '--theme', '12345']) + }) + + test('filters empty strings from split', async () => { + // When + await handler({command: ' theme list '}) + + // Then + expect(mockExecShopify).toHaveBeenCalledWith(config, ['theme', 'list']) + }) + + test('blocks long-running commands with helpful message', async () => { + // When + const result = await handler({command: 'theme dev --store my-store.myshopify.com'}) + + // Then + expect(mockExecShopify).not.toHaveBeenCalled() + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('long-running') + expect(result.content[0]!.text).toContain('separate terminal') + }) + + test('blocks app dev as long-running', async () => { + // When + const result = await handler({command: 'app dev'}) + + // Then + expect(mockExecShopify).not.toHaveBeenCalled() + expect(result.isError).toBe(true) + }) + + test('passes result through formatToolResult', async () => { + // Given + const execResult = {stdout: '{"id":1}', stderr: '', exitCode: 0} + mockExecShopify.mockResolvedValue(execResult) + + // When + await handler({command: 'theme info --json'}) + + // Then + expect(mockFormatToolResult).toHaveBeenCalledWith(execResult) + }) +}) diff --git a/packages/mcp-server/src/tools/cli-run.ts b/packages/mcp-server/src/tools/cli-run.ts new file mode 100644 index 00000000000..759e8722614 --- /dev/null +++ b/packages/mcp-server/src/tools/cli-run.ts @@ -0,0 +1,42 @@ +import {execShopify, formatToolResult, isLongRunningCommand} from '../subprocess.js' +import {z} from 'zod' +import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' +import type {McpServerConfig} from '../config.js' + +export function registerCliRun(server: McpServer, config: McpServerConfig): void { + server.registerTool( + 'shopify_cli_run', + { + description: + 'Run any Shopify CLI command. Automatically appends --no-color and injects store/auth from server config. Use shopify_cli_help to discover available commands and flags.', + inputSchema: { + command: z + .string() + .describe( + 'The CLI command to run after "shopify" (e.g. "theme list --json", "theme push --force --json", "theme check --output json")', + ), + }, + }, + async (params) => { + const args = params.command.split(/\s+/).filter(Boolean) + + const longRunning = isLongRunningCommand(args) + if (longRunning) { + return { + isError: true, + content: [ + { + type: 'text' as const, + text: `"shopify ${longRunning}" is a long-running dev server process that streams output continuously. Start it in a separate terminal instead:\n\nshopify ${longRunning}${ + config.store ? ` --store ${config.store}` : '' + }`, + }, + ], + } + } + + const result = await execShopify(config, args) + return formatToolResult(result) + }, + ) +} diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts new file mode 100644 index 00000000000..98ed20accc6 --- /dev/null +++ b/packages/mcp-server/src/tools/index.ts @@ -0,0 +1,11 @@ +import {registerAuthLogin} from './auth-login.js' +import {registerCliHelp} from './cli-help.js' +import {registerCliRun} from './cli-run.js' +import type {McpServerConfig} from '../config.js' +import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' + +export function registerAllTools(server: McpServer, config: McpServerConfig): void { + registerAuthLogin(server, config) + registerCliHelp(server, config) + registerCliRun(server, config) +} diff --git a/packages/mcp-server/tsconfig.build.json b/packages/mcp-server/tsconfig.build.json new file mode 100644 index 00000000000..d472b2c2aa0 --- /dev/null +++ b/packages/mcp-server/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.test.ts"] +} diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json new file mode 100644 index 00000000000..2d1cc58816c --- /dev/null +++ b/packages/mcp-server/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../configurations/tsconfig.json", + "include": ["./src/**/*.ts"], + "exclude": ["./dist"], + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + } +} diff --git a/packages/mcp-server/vite.config.ts b/packages/mcp-server/vite.config.ts new file mode 100644 index 00000000000..9536586ca45 --- /dev/null +++ b/packages/mcp-server/vite.config.ts @@ -0,0 +1,3 @@ +import config from '../../configurations/vite.config' + +export default config(__dirname) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 628b9da743f..d7a50bd6a59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -663,6 +663,22 @@ importers: specifier: ^1.0.1 version: 1.0.1 + packages/mcp-server: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.12.0 + version: 1.26.0(zod@3.25.76) + execa: + specifier: ^7.2.0 + version: 7.2.0 + zod: + specifier: ^3.25.0 + version: 3.25.76 + devDependencies: + '@vitest/coverage-istanbul': + specifier: ^3.1.4 + version: 3.2.1(vitest@3.2.1(@types/node@24.7.0)(jiti@2.4.2)(jsdom@20.0.3)(msw@2.8.7(@types/node@24.7.0)(typescript@5.8.3))(sass@1.89.1)(yaml@2.8.2)) + packages/plugin-cloudflare: dependencies: '@oclif/core': @@ -2834,6 +2850,12 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -3221,6 +3243,16 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mswjs/interceptors@0.38.7': resolution: {integrity: sha512-Jkb27iSn7JPdkqlTqKfhncFfnEZsIJVYxsFbUSWEkxdIPdsyngrhoDBk0/BGD2FQcRH99vlRrkHpNTyKqI+0/w==} engines: {node: '>=18'} @@ -4833,6 +4865,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -4874,6 +4910,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -5164,6 +5208,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -5539,6 +5587,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -5559,6 +5611,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -5576,6 +5632,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -6289,6 +6349,14 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@7.2.0: resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} @@ -6297,10 +6365,20 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -6401,6 +6479,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-nearest-file@1.1.0: resolution: {integrity: sha512-NMsS0ITOwpBPrHOyO7YUtDhaVEGUKS0kBJDVaWZPuCzO7JMW+uzFQQVts/gPyIV9ioyNWDb5LjhHWXVf1OnBDA==} @@ -6491,6 +6573,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + front-matter@4.0.2: resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} @@ -6797,6 +6883,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.11.8: + resolution: {integrity: sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==} + engines: {node: '>=16.9.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -6822,6 +6912,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -6975,6 +7069,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -7145,6 +7243,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -7319,6 +7420,9 @@ packages: jose@5.9.6: resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -7380,6 +7484,9 @@ packages: json-schema-typed@8.0.1: resolution: {integrity: sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -7657,6 +7764,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + meow@6.1.1: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} @@ -7664,6 +7775,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -7692,10 +7807,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -7845,6 +7968,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + network-interfaces@1.1.0: resolution: {integrity: sha512-fBk/Cm/RminFKhyUYKolI5nWI2de1m0pHlikz1mnTDbbe/1d2+ti+x/pWlOYuK8o/9p9vyK912+66h2NXGNUwQ==} @@ -8289,6 +8416,9 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -8339,6 +8469,10 @@ packages: resolution: {integrity: sha512-LFDwmhyWLBnmwO/2UFbWu1jEGVDzaPupaVdx0XcZ3tIAx1EDEBauzxXf2S0UcFK7oe+X9MApjH0hx9U1XMgfCA==} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@5.0.0: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} @@ -8453,6 +8587,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} @@ -8495,6 +8633,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -8777,6 +8919,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -8859,6 +9005,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} @@ -8870,6 +9020,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -9059,6 +9213,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -9467,6 +9625,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -9993,6 +10155,11 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@3.4.1: resolution: {integrity: sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw==} engines: {node: '>=18.0.0'} @@ -10002,6 +10169,9 @@ packages: zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: '@actions/core@1.11.1': @@ -13072,6 +13242,10 @@ snapshots: dependencies: graphql: 16.10.0 + '@hono/node-server@1.19.9(hono@4.11.8)': + dependencies: + hono: 4.11.8 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -13504,6 +13678,28 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.8) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.8 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.38.7': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -15653,6 +15849,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-globals@7.0.1: dependencies: acorn: 8.15.0 @@ -15688,6 +15889,10 @@ snapshots: optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -16058,6 +16263,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@8.1.1) + http-errors: 2.0.0 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolean@3.2.0: {} bottleneck@2.19.5: {} @@ -16482,6 +16701,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} convert-source-map@2.0.0: {} @@ -16494,6 +16715,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} cookie@0.7.2: {} @@ -16508,6 +16731,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -17394,6 +17622,12 @@ snapshots: eventemitter3@5.0.1: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@7.2.0: dependencies: cross-spawn: 7.0.6 @@ -17408,6 +17642,11 @@ snapshots: expect-type@1.2.1: {} + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -17444,6 +17683,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.0(supports-color@8.1.1) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extendable-error@0.1.7: {} fast-content-type-parse@2.0.1: {} @@ -17552,6 +17824,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.0(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-nearest-file@1.1.0: {} find-replace@3.0.0: @@ -17640,6 +17923,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + front-matter@4.0.2: dependencies: js-yaml: 3.14.2 @@ -18029,6 +18314,8 @@ snapshots: dependencies: react-is: 16.13.1 + hono@4.11.8: {} + hosted-git-info@2.8.9: {} hosted-git-info@7.0.2: @@ -18062,6 +18349,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 @@ -18265,6 +18560,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + ip-address@10.0.1: {} + ipaddr.js@1.9.1: {} iron-webcrypto@1.2.1: {} @@ -18401,6 +18698,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -18587,6 +18886,8 @@ snapshots: jose@5.9.6: {} + jose@6.1.3: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -18665,6 +18966,8 @@ snapshots: json-schema-typed@8.0.1: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-deterministic@1.0.12: {} @@ -18731,8 +19034,8 @@ snapshots: smol-toml: 1.3.4 strip-json-comments: 5.0.1 typescript: 5.8.3 - zod: 3.24.1 - zod-validation-error: 3.4.1(zod@3.24.1) + zod: 3.25.76 + zod-validation-error: 3.4.1(zod@3.25.76) knuth-shuffle-seeded@1.0.6: dependencies: @@ -18941,6 +19244,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + meow@6.1.1: dependencies: '@types/minimist': 1.2.5 @@ -18957,6 +19262,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -18979,10 +19286,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@3.0.0: {} @@ -19127,6 +19440,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + network-interfaces@1.1.0: {} no-case@3.0.4: @@ -19629,6 +19944,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@1.1.1: {} @@ -19680,6 +19997,8 @@ snapshots: quick-format-unescaped: 1.1.2 split2: 2.2.0 + pkce-challenge@5.0.1: {} + pkg-dir@5.0.0: dependencies: find-up: 5.0.0 @@ -19804,6 +20123,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + quansync@0.2.10: {} query-string@7.1.3: @@ -19840,6 +20163,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -20186,6 +20516,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.0(supports-color@8.1.1) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-async@2.4.1: optional: true @@ -20279,6 +20619,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + 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 + transitivePeerDependencies: + - supports-color + sentence-case@3.0.4: dependencies: no-case: 3.0.4 @@ -20298,6 +20654,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -20518,6 +20883,8 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + std-env@3.9.0: {} stop-iteration-iterator@1.1.0: @@ -20932,6 +21299,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -21596,8 +21969,14 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 - zod-validation-error@3.4.1(zod@3.24.1): + zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: - zod: 3.24.1 + zod: 3.25.76 + + zod-validation-error@3.4.1(zod@3.25.76): + dependencies: + zod: 3.25.76 zod@3.24.1: {} + + zod@3.25.76: {}