diff --git a/.changeset/config.json b/.changeset/config.json index b7767ea265..de23273b2a 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -11,7 +11,8 @@ "@shopify/theme", "@shopify/ui-extensions-dev-console-app", "@shopify/plugin-cloudflare", - "@shopify/plugin-did-you-mean" + "@shopify/plugin-did-you-mean", + "@shopify/mcp" ]], "access": "public", "baseBranch": "main", diff --git a/package.json b/package.json index 43063efb40..e6922c9f2f 100644 --- a/package.json +++ b/package.json @@ -229,6 +229,20 @@ ] } }, + "packages/mcp": { + "entry": [ + "**/src/index.ts!" + ], + "project": "**/*.ts!", + "ignoreDependencies": [ + "zod-to-json-schema" + ], + "vite": { + "config": [ + "vite.config.ts" + ] + } + }, "packages/plugin-did-you-mean": { "entry": [ "**/{commands,hooks}/**/*.ts!", diff --git a/packages/cli-kit/src/public/node/mcp.ts b/packages/cli-kit/src/public/node/mcp.ts new file mode 100644 index 0000000000..c77d6ccb5e --- /dev/null +++ b/packages/cli-kit/src/public/node/mcp.ts @@ -0,0 +1,110 @@ +import {identityFqdn} from './context/fqdn.js' +import {BugError} from './error.js' +import {shopifyFetch} from './http.js' +import {clientId, applicationId} from '../../private/node/session/identity.js' +import {pollForDeviceAuthorization} from '../../private/node/session/device-authorization.js' +import {exchangeAccessForApplicationTokens, ExchangeScopes} from '../../private/node/session/exchange.js' +import {allDefaultScopes, apiScopes} from '../../private/node/session/scopes.js' +import * as sessionStore from '../../private/node/session/store.js' +import {setCurrentSessionId} from '../../private/node/conf-store.js' + +import type {AdminSession} from './session.js' + +export interface DeviceCodeResponse { + deviceCode: string + userCode: string + verificationUri: string + verificationUriComplete: string + expiresIn: number + interval: number +} + +/** + * Requests a device authorization code for MCP non-interactive auth. + * + * @returns The device code response with verification URL. + */ +export async function requestDeviceCode(): Promise { + const fqdn = await identityFqdn() + const identityClientId = clientId() + const scopes = allDefaultScopes() + const params = new URLSearchParams({client_id: identityClientId, scope: scopes.join(' ')}).toString() + const url = `https://${fqdn}/oauth/device_authorization` + + const response = await shopifyFetch(url, { + method: 'POST', + headers: {'Content-type': 'application/x-www-form-urlencoded'}, + body: params, + }) + + const responseText = await response.text() + let result: Record + try { + result = JSON.parse(responseText) as Record + } catch { + throw new BugError(`Invalid response from authorization service (HTTP ${response.status})`) + } + + if (!result.device_code || !result.verification_uri_complete) { + throw new BugError('Failed to start device authorization') + } + + return { + deviceCode: result.device_code as string, + userCode: result.user_code as string, + verificationUri: result.verification_uri as string, + verificationUriComplete: result.verification_uri_complete as string, + expiresIn: result.expires_in as number, + interval: (result.interval as number) ?? 5, + } +} + +/** + * Completes device authorization by polling for approval and exchanging tokens. + * + * @param deviceCode - The device code from requestDeviceCode. + * @param interval - Polling interval in seconds. + * @param storeFqdn - The normalized store FQDN. + * @returns An admin session with token and store FQDN. + */ +export async function completeDeviceAuth( + deviceCode: string, + interval: number, + storeFqdn: string, +): Promise { + const identityToken = await pollForDeviceAuthorization(deviceCode, interval) + + const exchangeScopes: ExchangeScopes = { + admin: apiScopes('admin'), + partners: apiScopes('partners'), + storefront: apiScopes('storefront-renderer'), + businessPlatform: apiScopes('business-platform'), + appManagement: apiScopes('app-management'), + } + + const appTokens = await exchangeAccessForApplicationTokens(identityToken, exchangeScopes, storeFqdn) + + const fqdn = await identityFqdn() + const sessions = (await sessionStore.fetch()) ?? {} + const newSession = { + identity: identityToken, + applications: appTokens, + } + + const updatedSessions = { + ...sessions, + [fqdn]: {...sessions[fqdn], [identityToken.userId]: newSession}, + } + await sessionStore.store(updatedSessions) + setCurrentSessionId(identityToken.userId) + + const adminAppId = applicationId('admin') + const adminKey = `${storeFqdn}-${adminAppId}` + const adminToken = appTokens[adminKey] + + if (!adminToken) { + throw new BugError(`No admin token received for store ${storeFqdn}`) + } + + return {token: adminToken.accessToken, storeFqdn} +} diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 0000000000..ba91abb776 --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,50 @@ +# @shopify/mcp + +MCP server for the Shopify Admin API. Connects AI coding agents (Claude, Cursor, etc.) to your Shopify store via the [Model Context Protocol](https://modelcontextprotocol.io). + +## Setup + +```bash +claude mcp add shopify -- npx -y -p @shopify/mcp +``` + +Optionally set a default store so you don't have to pass it with every request: + +```bash +export SHOPIFY_FLAG_STORE=my-store.myshopify.com +``` + +## Tools + +### `shopify_auth_login` + +Authenticate with a Shopify store. Returns a URL the user must visit to complete login via device auth. After approval, subsequent `shopify_graphql` calls will use the session automatically. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `store` | string | No | Store domain. Defaults to `SHOPIFY_FLAG_STORE` env var. | + +### `shopify_graphql` + +Execute a GraphQL query or mutation against the Shopify Admin API. Uses the latest supported API version. + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `query` | string | Yes | GraphQL query or mutation string | +| `variables` | object | No | GraphQL variables | +| `store` | string | No | Store domain override. Defaults to `SHOPIFY_FLAG_STORE` env var. | +| `allowMutations` | boolean | No | Must be `true` to execute mutations. Safety measure to prevent unintended changes. | + +## Example + +``` +Agent: "List my products" + +→ shopify_auth_login(store: "my-store.myshopify.com") +← "Open this URL to authenticate: https://accounts.shopify.com/activate?user_code=ABCD-EFGH" + +[user approves in browser] + +→ shopify_graphql(query: "{ products(first: 5) { edges { node { title } } } }") +← { "products": { "edges": [{ "node": { "title": "T-Shirt" } }, ...] } } +``` diff --git a/packages/mcp/bin/shopify-mcp.js b/packages/mcp/bin/shopify-mcp.js new file mode 100755 index 0000000000..33a61be8e6 --- /dev/null +++ b/packages/mcp/bin/shopify-mcp.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import '../dist/index.js' diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 0000000000..4fcbfaff08 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,72 @@ +{ + "name": "@shopify/mcp", + "version": "3.91.0", + "description": "MCP server for the Shopify Admin API", + "packageManager": "pnpm@10.11.1", + "private": false, + "keywords": [ + "shopify", + "mcp", + "model-context-protocol" + ], + "homepage": "https://github.com/shopify/cli#readme", + "bugs": { + "url": "https://community.shopify.dev/c/shopify-cli-libraries/14" + }, + "repository": { + "type": "git", + "url": "https://github.com/Shopify/cli.git", + "directory": "packages/mcp" + }, + "license": "MIT", + "type": "module", + "bin": { + "shopify-mcp": "./bin/shopify-mcp.js" + }, + "files": [ + "/bin", + "/dist" + ], + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "nx build", + "clean": "nx clean", + "lint": "nx lint", + "lint:fix": "nx lint:fix", + "prepack": "NODE_ENV=production pnpm nx build && cp ../../README.md README.md", + "vitest": "vitest", + "type-check": "nx type-check" + }, + "eslintConfig": { + "extends": [ + "../../.eslintrc.cjs" + ] + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.22.0", + "@shopify/cli-kit": "3.91.0", + "zod": "3.24.1", + "zod-to-json-schema": "3.24.5" + }, + "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/project.json b/packages/mcp/project.json new file mode 100644 index 0000000000..1445f460e0 --- /dev/null +++ b/packages/mcp/project.json @@ -0,0 +1,46 @@ +{ + "name": "mcp", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/mcp/src", + "projectType": "library", + "tags": ["scope:feature"], + "targets": { + "clean": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm rimraf dist/", + "cwd": "packages/mcp" + } + }, + "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" + } + }, + "lint": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm eslint \"src/**/*.ts\"", + "cwd": "packages/mcp" + } + }, + "lint:fix": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm eslint 'src/**/*.ts' --fix", + "cwd": "packages/mcp" + } + }, + "type-check": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm tsc --noEmit", + "cwd": "packages/mcp" + } + } + } +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 0000000000..8fb32f37e4 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,15 @@ +import {createServer} from './server.js' +import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js' + +const server = createServer() +const transport = new StdioServerTransport() +await server.connect(transport) + +const shutdown = () => { + const _closing = server + .close() + .then(() => process.exit(0)) + .catch(() => process.exit(1)) +} +process.on('SIGINT', shutdown) +process.on('SIGTERM', shutdown) diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts new file mode 100644 index 0000000000..9d342e734c --- /dev/null +++ b/packages/mcp/src/server.ts @@ -0,0 +1,23 @@ +import {SessionManager} from './session-manager.js' +import {registerAuthTool} from './tools/auth.js' +import {registerGraphqlTool} from './tools/graphql.js' +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' + +import {createRequire} from 'module' + +const require = createRequire(import.meta.url) +const {version} = require('../package.json') as {version: string} + +export function createServer(): McpServer { + const server = new McpServer({ + name: 'shopify', + version, + }) + + const sessionManager = new SessionManager() + + registerAuthTool(server, sessionManager) + registerGraphqlTool(server, sessionManager) + + return server +} diff --git a/packages/mcp/src/session-manager.test.ts b/packages/mcp/src/session-manager.test.ts new file mode 100644 index 0000000000..56017c9d92 --- /dev/null +++ b/packages/mcp/src/session-manager.test.ts @@ -0,0 +1,199 @@ +import {SessionManager} from './session-manager.js' +import {AbortError} from '@shopify/cli-kit/node/error' +import {describe, test, expect, vi} from 'vitest' + +const mockEnsureAuthenticatedAdmin = vi.fn() +const mockRequestDeviceCode = vi.fn() +const mockCompleteDeviceAuth = vi.fn() +const mockNormalizeStoreFqdn = vi.fn((store: string) => `${store}.myshopify.com`) + +vi.mock('@shopify/cli-kit/node/session', () => ({ + ensureAuthenticatedAdmin: (...args: unknown[]) => mockEnsureAuthenticatedAdmin(...args), +})) + +vi.mock('@shopify/cli-kit/node/error', () => { + class MockAbortError extends Error { + constructor(message: string) { + super(message) + this.name = 'AbortError' + } + } + return {AbortError: MockAbortError} +}) + +vi.mock('@shopify/cli-kit/node/mcp', () => ({ + requestDeviceCode: (...args: unknown[]) => mockRequestDeviceCode(...args), + completeDeviceAuth: (...args: unknown[]) => mockCompleteDeviceAuth(...args), +})) + +vi.mock('@shopify/cli-kit/node/context/fqdn', () => ({ + normalizeStoreFqdn: (store: string) => mockNormalizeStoreFqdn(store), +})) + +describe('SessionManager', () => { + describe('getSession', () => { + test('returns existing session from ensureAuthenticatedAdmin', async () => { + const sessionManager = new SessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + mockEnsureAuthenticatedAdmin.mockResolvedValue(session) + + const result = await sessionManager.getSession('test') + expect(result).toEqual(session) + expect(mockEnsureAuthenticatedAdmin).toHaveBeenCalledWith('test.myshopify.com', [], {noPrompt: true}) + }) + + test('returns undefined when ensureAuthenticatedAdmin throws AbortError', async () => { + const sessionManager = new SessionManager() + mockEnsureAuthenticatedAdmin.mockRejectedValue(new AbortError('Unable to prompt')) + + const result = await sessionManager.getSession('test') + expect(result).toBeUndefined() + }) + + test('rethrows non-AbortError errors', async () => { + const sessionManager = new SessionManager() + mockEnsureAuthenticatedAdmin.mockRejectedValue(new TypeError('Unexpected null')) + + await expect(sessionManager.getSession('test')).rejects.toThrow('Unexpected null') + }) + + test('caches session after first successful fetch', async () => { + const sessionManager = new SessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + mockEnsureAuthenticatedAdmin.mockResolvedValueOnce(session) + + await sessionManager.getSession('test') + const result = await sessionManager.getSession('test') + expect(result).toEqual(session) + expect(mockEnsureAuthenticatedAdmin).toHaveBeenCalledTimes(1) + }) + }) + + describe('startAuth', () => { + test('returns device code response and starts background polling', async () => { + const sessionManager = new SessionManager() + const deviceCode = { + deviceCode: 'dev-123', + userCode: 'USR-123', + verificationUri: 'https://accounts.shopify.com/activate', + verificationUriComplete: 'https://accounts.shopify.com/activate?user_code=USR-123', + expiresIn: 600, + interval: 5, + } + mockRequestDeviceCode.mockResolvedValue(deviceCode) + + const session = {token: 'new-token', storeFqdn: 'test.myshopify.com'} + mockCompleteDeviceAuth.mockResolvedValue(session) + + const result = await sessionManager.startAuth('test') + expect(result).toEqual(deviceCode) + expect(mockRequestDeviceCode).toHaveBeenCalled() + expect(mockCompleteDeviceAuth).toHaveBeenCalledWith('dev-123', 5, 'test.myshopify.com') + }) + + test('throws when startAuth called concurrently for same store', async () => { + const sessionManager = new SessionManager() + const deviceCode = { + deviceCode: 'dev-123', + userCode: 'USR-123', + verificationUri: 'https://accounts.shopify.com/activate', + verificationUriComplete: 'https://accounts.shopify.com/activate?user_code=USR-123', + expiresIn: 600, + interval: 5, + } + mockRequestDeviceCode.mockResolvedValue(deviceCode) + mockCompleteDeviceAuth.mockReturnValue(new Promise(() => {})) + + await sessionManager.startAuth('test') + await expect(sessionManager.startAuth('test')).rejects.toThrow('Authentication already in progress') + }) + + test('cleans up pendingAuth on background auth failure', async () => { + const sessionManager = new SessionManager() + const deviceCode = { + deviceCode: 'dev-fail', + userCode: 'USR-FAIL', + verificationUri: 'https://accounts.shopify.com/activate', + verificationUriComplete: 'https://accounts.shopify.com/activate?user_code=USR-FAIL', + expiresIn: 600, + interval: 5, + } + mockRequestDeviceCode.mockResolvedValue(deviceCode) + mockCompleteDeviceAuth.mockRejectedValue(new Error('Access denied')) + + await sessionManager.startAuth('test') + await vi.waitFor(() => Promise.resolve()) + + mockEnsureAuthenticatedAdmin.mockRejectedValue(new AbortError('No session')) + await expect(sessionManager.requireSession('test')).rejects.toThrow( + 'Not authenticated for store test.myshopify.com', + ) + }) + }) + + describe('requireSession', () => { + test('returns cached session', async () => { + const sessionManager = new SessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + mockEnsureAuthenticatedAdmin.mockResolvedValue(session) + + await sessionManager.getSession('test') + const result = await sessionManager.requireSession('test') + expect(result).toEqual(session) + }) + + test('waits for pending auth', async () => { + const sessionManager = new SessionManager() + const deviceCode = { + deviceCode: 'dev-123', + userCode: 'USR-123', + verificationUri: 'https://accounts.shopify.com/activate', + verificationUriComplete: 'https://accounts.shopify.com/activate?user_code=USR-123', + expiresIn: 600, + interval: 5, + } + mockRequestDeviceCode.mockResolvedValue(deviceCode) + + const session = {token: 'new-token', storeFqdn: 'test.myshopify.com'} + mockCompleteDeviceAuth.mockResolvedValue(session) + + await sessionManager.startAuth('test') + const result = await sessionManager.requireSession('test') + expect(result).toEqual(session) + }) + + test('throws friendly message when AbortError (no session)', async () => { + const sessionManager = new SessionManager() + mockEnsureAuthenticatedAdmin.mockRejectedValue(new AbortError('Unable to prompt')) + + await expect(sessionManager.requireSession('test')).rejects.toThrow( + 'Not authenticated for store test.myshopify.com', + ) + }) + + test('rethrows non-AbortError errors in requireSession', async () => { + const sessionManager = new SessionManager() + mockEnsureAuthenticatedAdmin.mockRejectedValue(new TypeError('Network failure')) + + await expect(sessionManager.requireSession('test')).rejects.toThrow('Network failure') + }) + }) + + describe('clearSession', () => { + test('removes cached session so next call re-fetches', async () => { + const sessionManager = new SessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + mockEnsureAuthenticatedAdmin.mockResolvedValue(session) + + await sessionManager.getSession('test') + sessionManager.clearSession('test') + + const session2 = {token: 'xyz', storeFqdn: 'test.myshopify.com'} + mockEnsureAuthenticatedAdmin.mockResolvedValue(session2) + + const result = await sessionManager.getSession('test') + expect(result).toEqual(session2) + expect(mockEnsureAuthenticatedAdmin).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/packages/mcp/src/session-manager.ts b/packages/mcp/src/session-manager.ts new file mode 100644 index 0000000000..426379f02d --- /dev/null +++ b/packages/mcp/src/session-manager.ts @@ -0,0 +1,81 @@ +import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session' +import {requestDeviceCode, completeDeviceAuth} from '@shopify/cli-kit/node/mcp' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import type {AdminSession} from '@shopify/cli-kit/node/session' +import type {DeviceCodeResponse} from '@shopify/cli-kit/node/mcp' + +export class SessionManager { + private readonly sessions: Map = new Map() + private readonly pendingAuth: Map> = new Map() + + async getSession(store: string): Promise { + const storeFqdn = normalizeStoreFqdn(store) + const cached = this.sessions.get(storeFqdn) + if (cached) return cached + + try { + const session = await ensureAuthenticatedAdmin(storeFqdn, [], {noPrompt: true}) + this.sessions.set(storeFqdn, session) + return session + } catch (error) { + if (error instanceof AbortError) { + return undefined + } + throw error + } + } + + async startAuth(store: string): Promise { + const storeFqdn = normalizeStoreFqdn(store) + + if (this.pendingAuth.has(storeFqdn)) { + throw new Error(`Authentication already in progress for store ${storeFqdn}.`) + } + + const deviceCodeResponse = await requestDeviceCode() + + const authPromise = completeDeviceAuth(deviceCodeResponse.deviceCode, deviceCodeResponse.interval, storeFqdn).then( + (session) => { + this.sessions.set(storeFqdn, session) + this.pendingAuth.delete(storeFqdn) + return session + }, + (error: unknown) => { + this.pendingAuth.delete(storeFqdn) + throw error + }, + ) + + authPromise.catch(() => {}) + this.pendingAuth.set(storeFqdn, authPromise) + return deviceCodeResponse + } + + async requireSession(store: string): Promise { + const storeFqdn = normalizeStoreFqdn(store) + + const cached = this.sessions.get(storeFqdn) + if (cached) return cached + + const pending = this.pendingAuth.get(storeFqdn) + if (pending) return pending + + try { + const session = await ensureAuthenticatedAdmin(storeFqdn, [], {noPrompt: true}) + this.sessions.set(storeFqdn, session) + return session + } catch (error) { + if (error instanceof AbortError) { + throw new Error(`Not authenticated for store ${storeFqdn}. Call shopify_auth_login first.`) + } + throw error + } + } + + clearSession(store: string): void { + const storeFqdn = normalizeStoreFqdn(store) + this.sessions.delete(storeFqdn) + this.pendingAuth.delete(storeFqdn) + } +} diff --git a/packages/mcp/src/tools/auth.test.ts b/packages/mcp/src/tools/auth.test.ts new file mode 100644 index 0000000000..a6bf9b26e4 --- /dev/null +++ b/packages/mcp/src/tools/auth.test.ts @@ -0,0 +1,100 @@ +import {handleAuthLogin} from './auth.js' +import {describe, test, expect, vi, beforeEach, afterEach} from 'vitest' +import type {SessionManager} from '../session-manager.js' + +function createMockSessionManager(): SessionManager { + return { + getSession: vi.fn(), + startAuth: vi.fn(), + requireSession: vi.fn(), + clearSession: vi.fn(), + } as unknown as SessionManager +} + +describe('handleAuthLogin', () => { + const originalEnv = process.env + + beforeEach(() => { + process.env = {...originalEnv} + delete process.env.SHOPIFY_FLAG_STORE + }) + + afterEach(() => { + process.env = originalEnv + }) + + test('returns error when no store specified', async () => { + const sm = createMockSessionManager() + const result = await handleAuthLogin(sm, undefined) + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('No store specified') + }) + + test('uses SHOPIFY_FLAG_STORE env when no store param', async () => { + process.env.SHOPIFY_FLAG_STORE = 'env-store.myshopify.com' + const sm = createMockSessionManager() + ;(sm.getSession as ReturnType).mockResolvedValue({token: 'abc', storeFqdn: 'env-store.myshopify.com'}) + + const result = await handleAuthLogin(sm, undefined) + + expect(result.isError).toBeUndefined() + expect(result.content[0]!.text).toContain('Already authenticated') + expect(sm.getSession).toHaveBeenCalledWith('env-store.myshopify.com') + }) + + test('returns already authenticated when session exists', async () => { + const sm = createMockSessionManager() + ;(sm.getSession as ReturnType).mockResolvedValue({token: 'abc', storeFqdn: 'test.myshopify.com'}) + + const result = await handleAuthLogin(sm, 'test.myshopify.com') + + expect(result.isError).toBeUndefined() + expect(result.content[0]!.text).toContain('Already authenticated with store test.myshopify.com') + }) + + test('returns verification URL on new auth', async () => { + const sm = createMockSessionManager() + ;(sm.getSession as ReturnType).mockResolvedValue(undefined) + ;(sm.startAuth as ReturnType).mockResolvedValue({ + deviceCode: 'dev-123', + userCode: 'USR-123', + verificationUriComplete: 'https://accounts.shopify.com/activate?user_code=USR-123', + interval: 5, + }) + + const result = await handleAuthLogin(sm, 'test.myshopify.com') + + expect(result.isError).toBeUndefined() + expect(result.content[0]!.text).toContain('https://accounts.shopify.com/activate?user_code=USR-123') + expect(result.content[0]!.text).toContain('USR-123') + expect(result.content[0]!.text).toContain('After approving') + }) + + test('returns error on auth failure', async () => { + const sm = createMockSessionManager() + ;(sm.getSession as ReturnType).mockResolvedValue(undefined) + ;(sm.startAuth as ReturnType).mockRejectedValue(new Error('Network timeout')) + + const result = await handleAuthLogin(sm, 'test.myshopify.com') + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('Network timeout') + }) + + test('sanitizes tokens and paths in error messages', async () => { + const sm = createMockSessionManager() + ;(sm.getSession as ReturnType).mockResolvedValue(undefined) + ;(sm.startAuth as ReturnType).mockRejectedValue( + new Error('Failed with Bearer abc123token at /Users/dev/secret/path'), + ) + + const result = await handleAuthLogin(sm, 'test.myshopify.com') + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('Bearer [REDACTED]') + expect(result.content[0]!.text).toContain('[PATH]') + expect(result.content[0]!.text).not.toContain('abc123token') + expect(result.content[0]!.text).not.toContain('/Users/dev') + }) +}) diff --git a/packages/mcp/src/tools/auth.ts b/packages/mcp/src/tools/auth.ts new file mode 100644 index 0000000000..33e427650f --- /dev/null +++ b/packages/mcp/src/tools/auth.ts @@ -0,0 +1,68 @@ +import type {SessionManager} from '../session-manager.js' +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' +import {z} from 'zod' + +export interface ToolResult { + [key: string]: unknown + content: Array<{type: 'text'; text: string}> + isError?: boolean +} + +export function resolveStore(store: string | undefined): string | undefined { + return store ?? process.env.SHOPIFY_FLAG_STORE +} + +export async function handleAuthLogin(sessionManager: SessionManager, store: string | undefined): Promise { + const resolvedStore = resolveStore(store) + if (!resolvedStore) { + return { + content: [{type: 'text', text: 'Error: No store specified. Provide a store parameter or set SHOPIFY_FLAG_STORE environment variable.'}], + isError: true, + } + } + + try { + const existing = await sessionManager.getSession(resolvedStore) + if (existing) { + return { + content: [{type: 'text', text: `Already authenticated with store ${existing.storeFqdn}.`}], + } + } + + const deviceCode = await sessionManager.startAuth(resolvedStore) + return { + content: [ + { + type: 'text', + text: [ + 'To authenticate, open this URL in your browser:', + '', + deviceCode.verificationUriComplete, + '', + `User code: ${deviceCode.userCode}`, + '', + 'After approving in the browser, try your next request -- authentication will complete automatically.', + ].join('\n'), + }, + ], + } + } catch (error) { + const rawMessage = error instanceof Error ? error.message : String(error) + const message = rawMessage.replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]').replace(/\/Users\/[^\s]+/g, '[PATH]') + return { + content: [{type: 'text', text: `Authentication error: ${message}`}], + isError: true, + } + } +} + +export function registerAuthTool(server: McpServer, sessionManager: SessionManager) { + server.tool( + 'shopify_auth_login', + 'Authenticate with a Shopify store. Returns a URL the user must visit to complete login. After approval, subsequent shopify_graphql calls will automatically use the session.', + { + store: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\-.]*$/).optional().describe('Store domain (e.g. "my-store.myshopify.com"). Must match /^[a-zA-Z0-9][a-zA-Z0-9\\-.]*$/. Defaults to SHOPIFY_FLAG_STORE env var.'), + }, + ({store}) => handleAuthLogin(sessionManager, store), + ) +} diff --git a/packages/mcp/src/tools/graphql.test.ts b/packages/mcp/src/tools/graphql.test.ts new file mode 100644 index 0000000000..a17cde27d3 --- /dev/null +++ b/packages/mcp/src/tools/graphql.test.ts @@ -0,0 +1,182 @@ +import {handleGraphql} from './graphql.js' +import {describe, test, expect, vi, beforeEach, afterEach} from 'vitest' +import type {SessionManager} from '../session-manager.js' + +const mockAdminRequest = vi.fn() + +vi.mock('@shopify/cli-kit/node/api/admin', () => ({ + adminRequest: (...args: unknown[]) => mockAdminRequest(...args), +})) + +function createMockSessionManager(): SessionManager { + return { + getSession: vi.fn(), + startAuth: vi.fn(), + requireSession: vi.fn(), + clearSession: vi.fn(), + } as unknown as SessionManager +} + +describe('handleGraphql', () => { + const originalEnv = process.env + + beforeEach(() => { + process.env = {...originalEnv} + delete process.env.SHOPIFY_FLAG_STORE + }) + + afterEach(() => { + process.env = originalEnv + }) + + test('returns error when no store specified', async () => { + const sm = createMockSessionManager() + const result = await handleGraphql(sm, {query: '{ shop { name } }', allowMutations: false}) + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('No store specified') + }) + + test('blocks mutations without allowMutations flag', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + + const result = await handleGraphql(sm, {query: 'mutation { productDelete(input: {id: "1"}) { deletedProductId } }', allowMutations: false}) + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('allowMutations: true') + }) + + test('allows mutations with allowMutations flag', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + ;(sm.requireSession as ReturnType).mockResolvedValue(session) + mockAdminRequest.mockResolvedValue({data: {productDelete: {deletedProductId: '1'}}}) + + const result = await handleGraphql(sm, { + query: 'mutation { productDelete(input: {id: "1"}) { deletedProductId } }', + allowMutations: true, + }) + + expect(result.isError).toBeUndefined() + expect(mockAdminRequest).toHaveBeenCalledWith('mutation { productDelete(input: {id: "1"}) { deletedProductId } }', session, undefined) + }) + + test('executes query and returns JSON result', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + ;(sm.requireSession as ReturnType).mockResolvedValue(session) + mockAdminRequest.mockResolvedValue({data: {shop: {name: 'Test Store'}}}) + + const result = await handleGraphql(sm, {query: '{ shop { name } }', allowMutations: false}) + + expect(result.isError).toBeUndefined() + const parsed = JSON.parse(result.content[0]!.text) + expect(parsed.data.shop.name).toBe('Test Store') + }) + + test('passes variables to adminRequest', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + ;(sm.requireSession as ReturnType).mockResolvedValue(session) + mockAdminRequest.mockResolvedValue({data: {node: {id: 'gid://shopify/Product/1'}}}) + + const variables = {id: 'gid://shopify/Product/1'} + await handleGraphql(sm, {query: 'query($id: ID!) { node(id: $id) { id } }', variables, allowMutations: false}) + + expect(mockAdminRequest).toHaveBeenCalledWith('query($id: ID!) { node(id: $id) { id } }', session, variables) + }) + + test('returns auth error when not authenticated', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + ;(sm.requireSession as ReturnType).mockRejectedValue(new Error('Not authenticated for store test.myshopify.com. Call shopify_auth_login first.')) + + const result = await handleGraphql(sm, {query: '{ shop { name } }', allowMutations: false}) + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('shopify_auth_login') + }) + + test('clears session and returns error on 401', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + const session = {token: 'expired', storeFqdn: 'test.myshopify.com'} + ;(sm.requireSession as ReturnType).mockResolvedValue(session) + mockAdminRequest.mockRejectedValue(new Error('401 Unauthorized')) + + const result = await handleGraphql(sm, {query: '{ shop { name } }', allowMutations: false}) + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('Session expired') + expect(sm.clearSession).toHaveBeenCalledWith('test.myshopify.com') + }) + + test('returns GraphQL error for non-auth failures', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + ;(sm.requireSession as ReturnType).mockResolvedValue(session) + mockAdminRequest.mockRejectedValue(new Error('Field "invalid" not found on type "Shop"')) + + const result = await handleGraphql(sm, {query: '{ shop { invalid } }', allowMutations: false}) + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('GraphQL error') + expect(result.content[0]!.text).toContain('Field "invalid" not found') + }) + + test('detects mutations with leading comments', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + const result = await handleGraphql(sm, { + query: '# comment\nmutation { productDelete(input: {id: "1"}) { deletedProductId } }', + allowMutations: false, + }) + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('allowMutations') + }) + + test('detects mutations after a query in multi-operation document', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + const result = await handleGraphql(sm, { + query: 'query { shop { name } }\nmutation { productDelete(input: {id: "1"}) { deletedProductId } }', + allowMutations: false, + }) + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('allowMutations') + }) + + test('mutation pattern detection', () => { + const pattern = /(?:^|\n)\s*mutation[\s({]/i + expect(pattern.test('mutation { shop { name } }')).toBe(true) + expect(pattern.test(' mutation CreateProduct($input: ProductInput!) { }')).toBe(true) + expect(pattern.test('mutation(')).toBe(true) + expect(pattern.test('MUTATION { }')).toBe(true) + expect(pattern.test('Mutation { }')).toBe(true) + expect(pattern.test('query { shop { name } }\nmutation { }')).toBe(true) + expect(pattern.test('query { shop { name } }')).toBe(false) + expect(pattern.test('{ shop { name } }')).toBe(false) + expect(pattern.test('query GetMutationResults { }')).toBe(false) + }) + + test('sanitizes tokens and paths in error messages', async () => { + process.env.SHOPIFY_FLAG_STORE = 'test.myshopify.com' + const sm = createMockSessionManager() + const session = {token: 'abc', storeFqdn: 'test.myshopify.com'} + ;(sm.requireSession as ReturnType).mockResolvedValue(session) + mockAdminRequest.mockRejectedValue(new Error('Request failed with Bearer secretToken123 at /Users/dev/secret')) + + const result = await handleGraphql(sm, {query: '{ shop { name } }', allowMutations: false}) + + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('Bearer [REDACTED]') + expect(result.content[0]!.text).toContain('[PATH]') + expect(result.content[0]!.text).not.toContain('secretToken123') + expect(result.content[0]!.text).not.toContain('/Users/dev') + }) +}) diff --git a/packages/mcp/src/tools/graphql.ts b/packages/mcp/src/tools/graphql.ts new file mode 100644 index 0000000000..a5a2ee778e --- /dev/null +++ b/packages/mcp/src/tools/graphql.ts @@ -0,0 +1,70 @@ +import type {SessionManager} from '../session-manager.js' +import type {ToolResult} from './auth.js' +import {resolveStore} from './auth.js' +import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' +import {z} from 'zod' +import {adminRequest} from '@shopify/cli-kit/node/api/admin' + +const MUTATION_PATTERN = /(?:^|\n)\s*mutation[\s({]/i + +export async function handleGraphql( + sessionManager: SessionManager, + params: {query: string; variables?: Record; store?: string; allowMutations: boolean}, +): Promise { + const resolvedStore = resolveStore(params.store) + if (!resolvedStore) { + return { + content: [{type: 'text', text: 'Error: No store specified. Provide a store parameter or set SHOPIFY_FLAG_STORE environment variable.'}], + isError: true, + } + } + + const stripped = params.query.replace(/#[^\n]*/g, '') + if (MUTATION_PATTERN.test(stripped) && !params.allowMutations) { + return { + content: [{type: 'text', text: 'Error: Mutations require allowMutations: true. This is a safety measure to prevent unintended changes.'}], + isError: true, + } + } + + try { + const session = await sessionManager.requireSession(resolvedStore) + const result = await adminRequest(params.query, session, params.variables) + return { + content: [{type: 'text', text: JSON.stringify(result, null, 2)}], + } + } catch (error) { + const rawMessage = error instanceof Error ? error.message : String(error) + const message = rawMessage.replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]').replace(/\/Users\/[^\s]+/g, '[PATH]') + const isAuthError = message.includes('Not authenticated') || message.includes('shopify_auth_login') + + if (isAuthError) { + return {content: [{type: 'text', text: message}], isError: true} + } + + const isExpiredToken = message.includes('401') || message.includes('Unauthorized') + if (isExpiredToken) { + sessionManager.clearSession(resolvedStore) + return { + content: [{type: 'text', text: 'Session expired. Call shopify_auth_login to re-authenticate.'}], + isError: true, + } + } + + return {content: [{type: 'text', text: `GraphQL error: ${message}`}], isError: true} + } +} + +export function registerGraphqlTool(server: McpServer, sessionManager: SessionManager) { + server.tool( + 'shopify_graphql', + 'Execute a GraphQL query or mutation against the Shopify Admin API. Requires authentication via shopify_auth_login first. Uses the latest supported API version. Set allowMutations: true to run mutations.', + { + query: z.string().describe('GraphQL query or mutation string'), + variables: z.record(z.unknown()).optional().describe('GraphQL variables'), + store: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\-.]*$/).optional().describe('Store domain override. Must match /^[a-zA-Z0-9][a-zA-Z0-9\\-.]*$/. Defaults to SHOPIFY_FLAG_STORE env var.'), + allowMutations: z.boolean().optional().default(false).describe('Must be true to execute mutations'), + }, + ({query, variables, store, allowMutations}) => handleGraphql(sessionManager, {query, variables, store, allowMutations}), + ) +} diff --git a/packages/mcp/tsconfig.build.json b/packages/mcp/tsconfig.build.json new file mode 100644 index 0000000000..16506ad61a --- /dev/null +++ b/packages/mcp/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.test.ts"], + "references": [ + {"path": "../cli-kit"} + ] +} diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 0000000000..ea7490fa22 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../configurations/tsconfig.json", + "include": ["./src/**/*.ts"], + "exclude": ["./dist"], + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "references": [ + {"path": "../cli-kit"} + ] +} diff --git a/packages/mcp/vite.config.ts b/packages/mcp/vite.config.ts new file mode 100644 index 0000000000..9536586ca4 --- /dev/null +++ b/packages/mcp/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 9494c8dfb3..0092a543a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -658,6 +658,25 @@ importers: specifier: ^1.0.1 version: 1.0.1 + packages/mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: 1.22.0 + version: 1.22.0 + '@shopify/cli-kit': + specifier: 3.91.0 + version: link:../cli-kit + zod: + specifier: 3.24.1 + version: 3.24.1 + zod-to-json-schema: + specifier: 3.24.5 + version: 3.24.5(zod@3.24.1) + devDependencies: + '@vitest/coverage-istanbul': + specifier: ^3.1.4 + version: 3.2.4(vitest@3.2.4(@types/node@22.19.11)(jiti@2.6.1)(jsdom@20.0.3)(msw@2.12.10(@types/node@22.19.11)(typescript@5.9.3))(sass@1.97.3)(yaml@2.7.0)) + packages/plugin-cloudflare: dependencies: '@oclif/core': @@ -2862,6 +2881,15 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@modelcontextprotocol/sdk@1.22.0': + resolution: {integrity: sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mswjs/interceptors@0.41.3': resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} engines: {node: '>=18'} @@ -4438,6 +4466,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==} @@ -4475,6 +4507,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.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} @@ -4767,6 +4807,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. @@ -5139,6 +5183,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'} @@ -5159,6 +5207,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'} @@ -5176,6 +5228,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'} @@ -5892,6 +5948,14 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + 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} @@ -5900,10 +5964,20 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + 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==} @@ -6005,6 +6079,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==} @@ -6095,6 +6173,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==} @@ -6420,6 +6502,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'} @@ -6730,6 +6816,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'} @@ -7211,6 +7300,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'} @@ -7218,6 +7311,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==} @@ -7246,10 +7343,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'} @@ -7406,6 +7511,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==} @@ -7852,6 +7961,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'} @@ -7902,6 +8014,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'} @@ -8016,6 +8132,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -8058,6 +8178,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 @@ -8336,6 +8460,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-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -8403,6 +8531,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==} @@ -8414,6 +8546,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==} @@ -9018,6 +9154,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'} @@ -9534,6 +9674,11 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + zod-validation-error@3.5.4: resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} engines: {node: '>=18.0.0'} @@ -12423,6 +12568,24 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@modelcontextprotocol/sdk@1.22.0': + dependencies: + 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: 7.5.1(express@5.2.1) + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.24.1 + zod-to-json-schema: 3.24.5(zod@3.24.1) + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.41.3': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -14434,6 +14597,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.16.0 @@ -14466,6 +14634,10 @@ snapshots: optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -14832,6 +15004,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.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolean@3.2.0: {} bottleneck@2.19.5: {} @@ -15251,6 +15437,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} convert-source-map@2.0.0: {} @@ -15263,6 +15451,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} cookie@1.1.1: {} @@ -15277,6 +15467,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 @@ -16119,6 +16314,12 @@ snapshots: eventemitter3@5.0.4: {} + 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 @@ -16133,6 +16334,10 @@ snapshots: expect-type@1.3.0: {} + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -16169,6 +16374,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.1 + 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.1 + 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.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extendable-error@0.1.7: {} fast-content-type-parse@2.0.1: {} @@ -16277,6 +16515,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.2 + transitivePeerDependencies: + - supports-color + find-nearest-file@1.1.0: {} find-replace@3.0.0: @@ -16364,6 +16613,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + front-matter@4.0.2: dependencies: js-yaml: 3.14.2 @@ -16785,6 +17036,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 @@ -17088,6 +17347,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 @@ -17590,6 +17851,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + meow@6.1.1: dependencies: '@types/minimist': 1.2.5 @@ -17606,6 +17869,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -17628,10 +17893,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: {} @@ -17781,6 +18052,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + network-interfaces@1.1.0: {} no-case@3.0.4: @@ -18278,6 +18551,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pathe@1.1.1: {} @@ -18329,6 +18604,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 @@ -18453,6 +18730,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} query-string@7.1.3: @@ -18489,6 +18770,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 @@ -18837,6 +19125,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 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-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -18917,6 +19215,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 @@ -18936,6 +19250,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: @@ -19569,6 +19892,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 @@ -20177,6 +20506,10 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 + zod-to-json-schema@3.24.5(zod@3.24.1): + dependencies: + zod: 3.24.1 + zod-validation-error@3.5.4(zod@3.24.1): dependencies: zod: 3.24.1