From 92d5a6d0bc9695444399eb54506a9af801b5ea2f Mon Sep 17 00:00:00 2001 From: Jeremy Gayed Date: Mon, 2 Mar 2026 09:54:16 -0500 Subject: [PATCH 1/2] Add MCP server package for Shopify Admin API New `@shopify/mcp` package that exposes the Shopify Admin API to AI coding agents via the Model Context Protocol (MCP). Uses cli-kit's existing auth and GraphQL infrastructure. Tools: - shopify_auth_login: device auth flow for non-interactive MCP context - shopify_graphql: execute Admin API queries/mutations Install: claude mcp add shopify -- npx -y -p @shopify/mcp Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/config.json | 3 +- packages/cli-kit/src/public/node/mcp.ts | 110 +++++++ packages/mcp/bin/shopify-mcp.js | 2 + packages/mcp/package.json | 71 +++++ packages/mcp/project.json | 46 +++ packages/mcp/src/index.ts | 15 + packages/mcp/src/server.ts | 23 ++ packages/mcp/src/session-manager.test.ts | 199 ++++++++++++ packages/mcp/src/session-manager.ts | 81 +++++ packages/mcp/src/tools/auth.test.ts | 100 ++++++ packages/mcp/src/tools/auth.ts | 68 ++++ packages/mcp/src/tools/graphql.test.ts | 170 ++++++++++ packages/mcp/src/tools/graphql.ts | 70 +++++ packages/mcp/tsconfig.build.json | 7 + packages/mcp/tsconfig.json | 13 + packages/mcp/vite.config.ts | 3 + pnpm-lock.yaml | 376 ++++++++++++++++++++++- 17 files changed, 1352 insertions(+), 5 deletions(-) create mode 100644 packages/cli-kit/src/public/node/mcp.ts create mode 100755 packages/mcp/bin/shopify-mcp.js create mode 100644 packages/mcp/package.json create mode 100644 packages/mcp/project.json create mode 100644 packages/mcp/src/index.ts create mode 100644 packages/mcp/src/server.ts create mode 100644 packages/mcp/src/session-manager.test.ts create mode 100644 packages/mcp/src/session-manager.ts create mode 100644 packages/mcp/src/tools/auth.test.ts create mode 100644 packages/mcp/src/tools/auth.ts create mode 100644 packages/mcp/src/tools/graphql.test.ts create mode 100644 packages/mcp/src/tools/graphql.ts create mode 100644 packages/mcp/tsconfig.build.json create mode 100644 packages/mcp/tsconfig.json create mode 100644 packages/mcp/vite.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index b7767ea2657..de23273b2ab 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/packages/cli-kit/src/public/node/mcp.ts b/packages/cli-kit/src/public/node/mcp.ts new file mode 100644 index 00000000000..8d62ad39e27 --- /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 = `client_id=${identityClientId}&scope=${scopes.join(' ')}` + 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/bin/shopify-mcp.js b/packages/mcp/bin/shopify-mcp.js new file mode 100755 index 00000000000..33a61be8e60 --- /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 00000000000..e3ba4606949 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,71 @@ +{ + "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.27.1", + "@shopify/cli-kit": "3.91.0", + "zod": "3.25.76" + }, + "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 00000000000..1445f460e03 --- /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 00000000000..8fb32f37e44 --- /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 00000000000..9d342e734c7 --- /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 00000000000..56017c9d92f --- /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 00000000000..426379f02d5 --- /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 00000000000..a6bf9b26e43 --- /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 00000000000..33e427650f3 --- /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 00000000000..bf8df1c63ab --- /dev/null +++ b/packages/mcp/src/tools/graphql.test.ts @@ -0,0 +1,170 @@ +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('mutation pattern detection', () => { + const pattern = /^\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 } }')).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 00000000000..26de5045dcd --- /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 = /^\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(/^\s*(#[^\n]*\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 00000000000..16506ad61a2 --- /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 00000000000..ea7490fa22f --- /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 00000000000..9536586ca45 --- /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 9494c8dfb30..ba954e253f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -658,6 +658,22 @@ importers: specifier: ^1.0.1 version: 1.0.1 + packages/mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: 1.27.1 + version: 1.27.1(zod@3.25.76) + '@shopify/cli-kit': + specifier: 3.91.0 + version: link:../cli-kit + zod: + specifier: 3.25.76 + version: 3.25.76 + 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': @@ -2606,6 +2622,12 @@ packages: peerDependencies: graphql: 16.10.0 + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2862,6 +2884,16 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mswjs/interceptors@0.41.3': resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} engines: {node: '>=18'} @@ -4438,6 +4470,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 +4511,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 +4811,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 +5187,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 +5211,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 +5232,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 +5952,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 +5968,20 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 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==} @@ -6005,6 +6083,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 +6177,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==} @@ -6391,6 +6477,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.12.3: + resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==} + engines: {node: '>=16.9.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -6420,6 +6510,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'} @@ -6557,6 +6651,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'} @@ -6730,6 +6828,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'} @@ -6896,6 +6997,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==} @@ -7211,6 +7315,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 +7326,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 +7358,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 +7526,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 +7976,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 +8029,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 +8147,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 +8193,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 +8475,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 +8546,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 +8561,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 +9169,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 +9689,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.5.4: resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} engines: {node: '>=18.0.0'} @@ -9543,6 +9703,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': @@ -12125,6 +12288,10 @@ snapshots: dependencies: graphql: 16.10.0 + '@hono/node-server@1.19.9(hono@4.12.3)': + dependencies: + hono: 4.12.3 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -12423,6 +12590,28 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.12.3) + 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.12.3 + 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.41.3': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -14434,6 +14623,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 +14660,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 +15030,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 +15463,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 +15477,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} cookie@1.1.1: {} @@ -15277,6 +15493,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 +16340,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 +16360,11 @@ snapshots: expect-type@1.3.0: {} + 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 @@ -16169,6 +16401,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 +16542,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 +16640,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + front-matter@4.0.2: dependencies: js-yaml: 3.14.2 @@ -16748,6 +17026,8 @@ snapshots: dependencies: react-is: 16.13.1 + hono@4.12.3: {} + hosted-git-info@2.8.9: {} hosted-git-info@7.0.2: @@ -16785,6 +17065,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 @@ -16947,6 +17235,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + ip-address@10.0.1: {} + ipaddr.js@1.9.1: {} iron-webcrypto@1.2.1: {} @@ -17088,6 +17378,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 @@ -17266,6 +17558,8 @@ snapshots: jose@5.9.6: {} + jose@6.1.3: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -17404,8 +17698,8 @@ snapshots: smol-toml: 1.6.0 strip-json-comments: 5.0.1 typescript: 5.9.3 - zod: 3.24.1 - zod-validation-error: 3.5.4(zod@3.24.1) + zod: 3.25.76 + zod-validation-error: 3.5.4(zod@3.25.76) knuth-shuffle-seeded@1.0.6: dependencies: @@ -17590,6 +17884,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + meow@6.1.1: dependencies: '@types/minimist': 1.2.5 @@ -17606,6 +17902,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -17628,10 +17926,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 +18085,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + network-interfaces@1.1.0: {} no-case@3.0.4: @@ -18278,6 +18584,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 +18637,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 +18763,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 +18803,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 +19158,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 +19248,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 +19283,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 +19925,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,8 +20539,14 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 - zod-validation-error@3.5.4(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.5.4(zod@3.25.76): + dependencies: + zod: 3.25.76 zod@3.24.1: {} + + zod@3.25.76: {} From aea68545635dbf50f9e71b6aff33bfb49dcfab7b Mon Sep 17 00:00:00 2001 From: Jeremy Gayed Date: Mon, 2 Mar 2026 09:54:38 -0500 Subject: [PATCH 2/2] Add README for @shopify/mcp package Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 14 ++++ packages/cli-kit/src/public/node/mcp.ts | 2 +- packages/mcp/README.md | 50 +++++++++++++++ packages/mcp/package.json | 5 +- packages/mcp/src/tools/graphql.test.ts | 14 +++- packages/mcp/src/tools/graphql.ts | 4 +- pnpm-lock.yaml | 85 ++++++++----------------- 7 files changed, 108 insertions(+), 66 deletions(-) create mode 100644 packages/mcp/README.md diff --git a/package.json b/package.json index 43063efb40a..e6922c9f2f8 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 index 8d62ad39e27..c77d6ccb5e5 100644 --- a/packages/cli-kit/src/public/node/mcp.ts +++ b/packages/cli-kit/src/public/node/mcp.ts @@ -28,7 +28,7 @@ export async function requestDeviceCode(): Promise { const fqdn = await identityFqdn() const identityClientId = clientId() const scopes = allDefaultScopes() - const params = `client_id=${identityClientId}&scope=${scopes.join(' ')}` + const params = new URLSearchParams({client_id: identityClientId, scope: scopes.join(' ')}).toString() const url = `https://${fqdn}/oauth/device_authorization` const response = await shopifyFetch(url, { diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 00000000000..ba91abb7765 --- /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/package.json b/packages/mcp/package.json index e3ba4606949..4fcbfaff082 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -48,9 +48,10 @@ ] }, "dependencies": { - "@modelcontextprotocol/sdk": "1.27.1", + "@modelcontextprotocol/sdk": "1.22.0", "@shopify/cli-kit": "3.91.0", - "zod": "3.25.76" + "zod": "3.24.1", + "zod-to-json-schema": "3.24.5" }, "devDependencies": { "@vitest/coverage-istanbul": "^3.1.4" diff --git a/packages/mcp/src/tools/graphql.test.ts b/packages/mcp/src/tools/graphql.test.ts index bf8df1c63ab..a17cde27d3c 100644 --- a/packages/mcp/src/tools/graphql.test.ts +++ b/packages/mcp/src/tools/graphql.test.ts @@ -140,13 +140,25 @@ describe('handleGraphql', () => { 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 = /^\s*mutation[\s({]/i + 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) diff --git a/packages/mcp/src/tools/graphql.ts b/packages/mcp/src/tools/graphql.ts index 26de5045dcd..a5a2ee778e9 100644 --- a/packages/mcp/src/tools/graphql.ts +++ b/packages/mcp/src/tools/graphql.ts @@ -5,7 +5,7 @@ import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' import {z} from 'zod' import {adminRequest} from '@shopify/cli-kit/node/api/admin' -const MUTATION_PATTERN = /^\s*mutation[\s({]/i +const MUTATION_PATTERN = /(?:^|\n)\s*mutation[\s({]/i export async function handleGraphql( sessionManager: SessionManager, @@ -19,7 +19,7 @@ export async function handleGraphql( } } - const stripped = params.query.replace(/^\s*(#[^\n]*\n)*/g, '') + 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.'}], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba954e253f3..0092a543a66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -661,14 +661,17 @@ importers: packages/mcp: dependencies: '@modelcontextprotocol/sdk': - specifier: 1.27.1 - version: 1.27.1(zod@3.25.76) + specifier: 1.22.0 + version: 1.22.0 '@shopify/cli-kit': specifier: 3.91.0 version: link:../cli-kit zod: - specifier: 3.25.76 - version: 3.25.76 + 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 @@ -2622,12 +2625,6 @@ packages: peerDependencies: graphql: 16.10.0 - '@hono/node-server@1.19.9': - resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2884,12 +2881,11 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} - '@modelcontextprotocol/sdk@1.27.1': - resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + '@modelcontextprotocol/sdk@1.22.0': + resolution: {integrity: sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 peerDependenciesMeta: '@cfworker/json-schema': optional: true @@ -5968,8 +5964,8 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.2.1: - resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -6477,10 +6473,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.12.3: - resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==} - engines: {node: '>=16.9.0'} - hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -6651,10 +6643,6 @@ 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'} @@ -6997,9 +6985,6 @@ 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==} @@ -9689,10 +9674,10 @@ 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==} + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} peerDependencies: - zod: ^3.25 || ^4 + zod: ^3.24.1 zod-validation-error@3.5.4: resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} @@ -9703,9 +9688,6 @@ 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': @@ -12288,10 +12270,6 @@ snapshots: dependencies: graphql: 16.10.0 - '@hono/node-server@1.19.9(hono@4.12.3)': - dependencies: - hono: 4.12.3 - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -12590,9 +12568,8 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} - '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.22.0': dependencies: - '@hono/node-server': 1.19.9(hono@4.12.3) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -12601,14 +12578,11 @@ snapshots: 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.12.3 - jose: 6.1.3 - json-schema-typed: 8.0.2 + express-rate-limit: 7.5.1(express@5.2.1) 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) + zod: 3.24.1 + zod-to-json-schema: 3.24.5(zod@3.24.1) transitivePeerDependencies: - supports-color @@ -16360,10 +16334,9 @@ snapshots: expect-type@1.3.0: {} - express-rate-limit@8.2.1(express@5.2.1): + express-rate-limit@7.5.1(express@5.2.1): dependencies: express: 5.2.1 - ip-address: 10.0.1 express@4.21.2: dependencies: @@ -17026,8 +16999,6 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.12.3: {} - hosted-git-info@2.8.9: {} hosted-git-info@7.0.2: @@ -17235,8 +17206,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - ip-address@10.0.1: {} - ipaddr.js@1.9.1: {} iron-webcrypto@1.2.1: {} @@ -17558,8 +17527,6 @@ snapshots: jose@5.9.6: {} - jose@6.1.3: {} - js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -17698,8 +17665,8 @@ snapshots: smol-toml: 1.6.0 strip-json-comments: 5.0.1 typescript: 5.9.3 - zod: 3.25.76 - zod-validation-error: 3.5.4(zod@3.25.76) + zod: 3.24.1 + zod-validation-error: 3.5.4(zod@3.24.1) knuth-shuffle-seeded@1.0.6: dependencies: @@ -20539,14 +20506,12 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 - zod-to-json-schema@3.25.1(zod@3.25.76): + zod-to-json-schema@3.24.5(zod@3.24.1): dependencies: - zod: 3.25.76 + zod: 3.24.1 - zod-validation-error@3.5.4(zod@3.25.76): + zod-validation-error@3.5.4(zod@3.24.1): dependencies: - zod: 3.25.76 + zod: 3.24.1 zod@3.24.1: {} - - zod@3.25.76: {}