add mcp for web (#6411)

* add mcp for web

* update /jan/v1 endpoint to /v1

* update mise and makefile

* update yarn lock

* use mcp oauth properly
This commit is contained in:
Dinh Long Nguyen 2025-09-12 12:14:10 +07:00 committed by GitHub
parent 72128117a9
commit b5b6e1dc19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 2670 additions and 74 deletions

View File

@ -40,9 +40,11 @@ install-web-app: config-yarn
yarn install
dev-web-app: install-web-app
yarn build:core
yarn dev:web-app
build-web-app: install-web-app
yarn build:core
yarn build:web-app
serve-web-app:

View File

@ -7,6 +7,7 @@ export enum ExtensionTypeEnum {
Inference = 'inference',
Model = 'model',
SystemMonitoring = 'systemMonitoring',
MCP = 'mcp',
HuggingFace = 'huggingFace',
Engine = 'engine',
Hardware = 'hardware',

View File

@ -14,6 +14,11 @@ export { InferenceExtension } from './inference'
*/
export { AssistantExtension } from './assistant'
/**
* MCP extension for managing tools and server communication.
*/
export { MCPExtension } from './mcp'
/**
* Base AI Engines.
*/

View File

@ -0,0 +1,99 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { MCPExtension } from './mcp'
import { ExtensionTypeEnum } from '../extension'
import { MCPTool, MCPToolCallResult } from '../../types'
class TestMCPExtension extends MCPExtension {
constructor() {
super('test://mcp', 'test-mcp')
}
async getTools(): Promise<MCPTool[]> {
return [
{
name: 'test_tool',
description: 'A test tool',
inputSchema: { type: 'object' },
server: 'test-server'
}
]
}
async callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult> {
return {
error: '',
content: [{ type: 'text', text: `Called ${toolName} with ${JSON.stringify(args)}` }]
}
}
async getConnectedServers(): Promise<string[]> {
return ['test-server']
}
async refreshTools(): Promise<void> {
// Mock implementation
}
async isHealthy(): Promise<boolean> {
return true
}
async onLoad(): Promise<void> {
// Mock implementation
}
onUnload(): void {
// Mock implementation
}
}
describe('MCPExtension', () => {
let mcpExtension: TestMCPExtension
beforeEach(() => {
mcpExtension = new TestMCPExtension()
})
describe('type', () => {
it('should return MCP extension type', () => {
expect(mcpExtension.type()).toBe(ExtensionTypeEnum.MCP)
})
})
describe('getTools', () => {
it('should return array of MCP tools', async () => {
const tools = await mcpExtension.getTools()
expect(tools).toHaveLength(1)
expect(tools[0]).toEqual({
name: 'test_tool',
description: 'A test tool',
inputSchema: { type: 'object' },
server: 'test-server'
})
})
})
describe('callTool', () => {
it('should call tool and return result', async () => {
const result = await mcpExtension.callTool('test_tool', { param: 'value' })
expect(result).toEqual({
error: '',
content: [{ type: 'text', text: 'Called test_tool with {"param":"value"}' }]
})
})
})
describe('getConnectedServers', () => {
it('should return list of connected servers', async () => {
const servers = await mcpExtension.getConnectedServers()
expect(servers).toEqual(['test-server'])
})
})
describe('isHealthy', () => {
it('should return health status', async () => {
const healthy = await mcpExtension.isHealthy()
expect(healthy).toBe(true)
})
})
})

View File

@ -0,0 +1,21 @@
import { MCPInterface, MCPTool, MCPToolCallResult } from '../../types'
import { BaseExtension, ExtensionTypeEnum } from '../extension'
/**
* MCP (Model Context Protocol) extension for managing tools and server communication.
* @extends BaseExtension
*/
export abstract class MCPExtension extends BaseExtension implements MCPInterface {
/**
* MCP extension type.
*/
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.MCP
}
abstract getTools(): Promise<MCPTool[]>
abstract callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult>
abstract getConnectedServers(): Promise<string[]>
abstract refreshTools(): Promise<void>
abstract isHealthy(): Promise<boolean>
}

View File

@ -10,3 +10,4 @@ export * from './api'
export * from './setting'
export * from './engine'
export * from './hardware'
export * from './mcp'

View File

@ -0,0 +1,2 @@
export * from './mcpEntity'
export * from './mcpInterface'

View File

@ -0,0 +1,24 @@
/**
* MCP (Model Context Protocol) entities
*/
export interface MCPTool {
name: string
description: string
inputSchema: Record<string, unknown>
server: string
}
export interface MCPToolCallResult {
error: string
content: Array<{
type?: string
text: string
}>
}
export interface MCPServerInfo {
name: string
connected: boolean
tools?: MCPTool[]
}

View File

@ -0,0 +1,32 @@
/**
* MCP (Model Context Protocol) interface
*/
import { MCPTool, MCPToolCallResult } from './mcpEntity'
export interface MCPInterface {
/**
* Get all available MCP tools
*/
getTools(): Promise<MCPTool[]>
/**
* Call a specific MCP tool
*/
callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult>
/**
* Get list of connected MCP servers
*/
getConnectedServers(): Promise<string[]>
/**
* Refresh the list of available tools
*/
refreshTools(): Promise<void>
/**
* Check if MCP service is healthy
*/
isHealthy(): Promise<boolean>
}

View File

@ -30,5 +30,8 @@
"peerDependencies": {
"@janhq/core": "*",
"zustand": "^5.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.5"
}
}

View File

@ -8,6 +8,7 @@ import type { WebExtensionRegistry } from './types'
export { default as AssistantExtensionWeb } from './assistant-web'
export { default as ConversationalExtensionWeb } from './conversational-web'
export { default as JanProviderWeb } from './jan-provider-web'
export { default as MCPExtensionWeb } from './mcp-web'
// Re-export types
export type {
@ -17,7 +18,8 @@ export type {
WebExtensionLoader,
AssistantWebModule,
ConversationalWebModule,
JanProviderWebModule
JanProviderWebModule,
MCPWebModule
} from './types'
// Extension registry for dynamic loading
@ -25,4 +27,5 @@ export const WEB_EXTENSIONS: WebExtensionRegistry = {
'assistant-web': () => import('./assistant-web'),
'conversational-web': () => import('./conversational-web'),
'jan-provider-web': () => import('./jan-provider-web'),
'mcp-web': () => import('./mcp-web'),
}

View File

@ -3,7 +3,7 @@
* Handles API requests to Jan backend for models and chat completions
*/
import { JanAuthService } from './auth'
import { JanAuthService } from '../shared/auth'
import { JanModel, janProviderStore } from './store'
// JAN_API_BASE is defined in vite.config.ts
@ -18,6 +18,7 @@ export interface JanChatMessage {
content: string
reasoning?: string
reasoning_content?: string
tool_calls?: any[]
}
export interface JanChatCompletionRequest {
@ -30,6 +31,8 @@ export interface JanChatCompletionRequest {
presence_penalty?: number
stream?: boolean
stop?: string | string[]
tools?: any[]
tool_choice?: any
}
export interface JanChatCompletionChoice {
@ -63,6 +66,7 @@ export interface JanChatCompletionChunk {
content?: string
reasoning?: string
reasoning_content?: string
tool_calls?: any[]
}
finish_reason: string | null
}>
@ -83,40 +87,12 @@ export class JanApiClient {
return JanApiClient.instance
}
private async makeAuthenticatedRequest<T>(
url: string,
options: RequestInit = {}
): Promise<T> {
try {
const authHeader = await this.authService.getAuthHeader()
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...authHeader,
...options.headers,
},
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
} catch (error) {
console.error('API request failed:', error)
throw error
}
}
async getModels(): Promise<JanModel[]> {
try {
janProviderStore.setLoadingModels(true)
janProviderStore.clearError()
const response = await this.makeAuthenticatedRequest<JanModelsResponse>(
const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>(
`${JAN_API_BASE}/models`
)
@ -138,7 +114,7 @@ export class JanApiClient {
try {
janProviderStore.clearError()
return await this.makeAuthenticatedRequest<JanChatCompletionResponse>(
return await this.authService.makeAuthenticatedRequest<JanChatCompletionResponse>(
`${JAN_API_BASE}/chat/completions`,
{
method: 'POST',

View File

@ -62,6 +62,7 @@ export default class JanProviderWeb extends AIEngine {
path: undefined, // Remote model, no local path
owned_by: model.owned_by,
object: model.object,
capabilities: ['tools'], // Jan models support both tools via MCP
}))
} catch (error) {
console.error('Failed to list Jan models:', error)
@ -150,6 +151,8 @@ export default class JanProviderWeb extends AIEngine {
presence_penalty: opts.presence_penalty ?? undefined,
stream: opts.stream ?? false,
stop: opts.stop ?? undefined,
tools: opts.tools ?? undefined,
tool_choice: opts.tool_choice ?? undefined,
}
if (opts.stream) {
@ -176,6 +179,7 @@ export default class JanProviderWeb extends AIEngine {
content: choice.message.content,
reasoning: choice.message.reasoning,
reasoning_content: choice.message.reasoning_content,
tool_calls: choice.message.tool_calls,
},
finish_reason: (choice.finish_reason || 'stop') as 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'function_call',
})),
@ -233,6 +237,7 @@ export default class JanProviderWeb extends AIEngine {
content: choice.delta.content,
reasoning: choice.delta.reasoning,
reasoning_content: choice.delta.reasoning_content,
tool_calls: choice.delta.tool_calls,
},
finish_reason: choice.finish_reason,
})),
@ -300,8 +305,9 @@ export default class JanProviderWeb extends AIEngine {
return Array.from(this.activeSessions.values()).map(session => session.model_id)
}
async isToolSupported(): Promise<boolean> {
// Tools are not yet supported
return false
async isToolSupported(modelId: string): Promise<boolean> {
// Jan models support tool calls via MCP
console.log(`Checking tool support for Jan model ${modelId}: supported`);
return true;
}
}

View File

@ -0,0 +1,237 @@
/**
* MCP Web Extension
* Provides Model Context Protocol functionality for web platform
* Uses official MCP TypeScript SDK with proper session handling
*/
import { MCPExtension, MCPTool, MCPToolCallResult } from '@janhq/core'
import { JanAuthService } from '../shared/auth'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { JanMCPOAuthProvider } from './oauth-provider'
// JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1')
declare const JAN_API_BASE: string
export default class MCPExtensionWeb extends MCPExtension {
private mcpEndpoint = '/mcp'
private tools: MCPTool[] = []
private initialized = false
private authService: JanAuthService
private mcpClient: Client | null = null
private oauthProvider: JanMCPOAuthProvider
constructor(
url: string,
name: string,
productName?: string,
active?: boolean,
description?: string,
version?: string
) {
super(url, name, productName, active, description, version)
this.authService = JanAuthService.getInstance()
this.oauthProvider = new JanMCPOAuthProvider(this.authService)
}
async onLoad(): Promise<void> {
try {
// Initialize authentication first
await this.authService.initialize()
// Initialize MCP client with OAuth
await this.initializeMCPClient()
// Then fetch tools
await this.initializeTools()
} catch (error) {
console.warn('Failed to initialize MCP extension:', error)
this.tools = []
}
}
async onUnload(): Promise<void> {
this.tools = []
this.initialized = false
// Close MCP client
if (this.mcpClient) {
try {
await this.mcpClient.close()
} catch (error) {
console.warn('Error closing MCP client:', error)
}
this.mcpClient = null
}
}
private async initializeMCPClient(): Promise<void> {
try {
// Close existing client if any
if (this.mcpClient) {
try {
await this.mcpClient.close()
} catch (error) {
// Ignore close errors
}
this.mcpClient = null
}
// Create transport with OAuth provider (handles token refresh automatically)
const transport = new StreamableHTTPClientTransport(
new URL(`${JAN_API_BASE}${this.mcpEndpoint}`),
{
authProvider: this.oauthProvider
// No sessionId needed - server will generate one automatically
}
)
// Create MCP client
this.mcpClient = new Client(
{
name: 'jan-web-client',
version: '1.0.0'
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
logging: {}
}
}
)
// Connect to MCP server (OAuth provider handles auth automatically)
await this.mcpClient.connect(transport)
console.log('MCP client connected successfully, session ID:', transport.sessionId)
} catch (error) {
console.error('Failed to initialize MCP client:', error)
throw error
}
}
private async initializeTools(): Promise<void> {
if (this.initialized || !this.mcpClient) {
return
}
try {
// Use MCP SDK to list tools
const result = await this.mcpClient.listTools()
console.log('MCP tools/list response:', result)
if (result.tools && Array.isArray(result.tools)) {
this.tools = result.tools.map((tool) => ({
name: tool.name,
description: tool.description || '',
inputSchema: (tool.inputSchema || {}) as Record<string, unknown>,
server: 'Jan MCP Server'
}))
} else {
console.warn('No tools found in MCP server response')
this.tools = []
}
this.initialized = true
console.log(`Initialized MCP extension with ${this.tools.length} tools:`, this.tools.map(t => t.name))
} catch (error) {
console.error('Failed to fetch MCP tools:', error)
this.tools = []
this.initialized = false
throw error
}
}
async getTools(): Promise<MCPTool[]> {
if (!this.initialized) {
await this.initializeTools()
}
return this.tools
}
async callTool(toolName: string, args: Record<string, unknown>): Promise<MCPToolCallResult> {
if (!this.mcpClient) {
return {
error: 'MCP client not initialized',
content: [{ type: 'text', text: 'MCP client not initialized' }]
}
}
try {
// Use MCP SDK to call tool (OAuth provider handles auth automatically)
const result = await this.mcpClient.callTool({
name: toolName,
arguments: args
})
console.log(`MCP tool call result for ${toolName}:`, result)
// Handle tool call result
if (result.isError) {
const errorText = Array.isArray(result.content) && result.content.length > 0
? (result.content[0].type === 'text' ? (result.content[0] as any).text : 'Tool call failed')
: 'Tool call failed'
return {
error: errorText,
content: [{ type: 'text', text: errorText }]
}
}
// Convert MCP content to Jan's format
const content = Array.isArray(result.content)
? result.content.map(item => {
if (item.type === 'text') {
return { type: 'text' as const, text: (item as any).text }
} else {
// For non-text types, convert to text representation
return { type: 'text' as const, text: JSON.stringify(item) }
}
})
: [{ type: 'text' as const, text: 'No content returned' }]
return {
error: '',
content
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Failed to call MCP tool ${toolName}:`, error)
return {
error: errorMessage,
content: [{ type: 'text', text: errorMessage }]
}
}
}
async isHealthy(): Promise<boolean> {
if (!this.mcpClient) {
return false
}
try {
// Try to list tools as health check (OAuth provider handles auth)
await this.mcpClient.listTools()
return true
} catch (error) {
console.warn('MCP health check failed:', error)
return false
}
}
async getConnectedServers(): Promise<string[]> {
// Return servers based on MCP client connection status
return this.mcpClient && this.initialized ? ['Jan MCP Server'] : []
}
async refreshTools(): Promise<void> {
this.initialized = false
try {
await this.initializeTools()
} catch (error) {
console.error('Failed to refresh tools:', error)
throw error
}
}
}

View File

@ -0,0 +1,60 @@
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
import { OAuthTokens, OAuthClientInformation, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'
import { JanAuthService } from '../shared/auth'
/**
* MCP OAuth provider that integrates with Jan Auth Service
* Just provides tokens, no storage or validation needed
*/
export class JanMCPOAuthProvider implements OAuthClientProvider {
private authService: JanAuthService
constructor(authService: JanAuthService) {
this.authService = authService
}
get redirectUrl(): string {
return ''
}
get clientMetadata(): OAuthClientMetadata {
return {
redirect_uris: [] // Not used, but required by interface
}
}
async clientInformation(): Promise<OAuthClientInformation | undefined> {
return undefined
}
async tokens(): Promise<OAuthTokens | undefined> {
try {
const accessToken = await this.authService.getValidAccessToken()
if (accessToken) {
return {
access_token: accessToken,
token_type: 'Bearer'
}
}
} catch (error) {
console.warn('Failed to get tokens from auth service:', error)
}
return undefined
}
async saveTokens(): Promise<void> {
// No-op: Jan auth service handles token storage
}
redirectToAuthorization(): void {
// No-op: Not handling authorization flow
}
async saveCodeVerifier(): Promise<void> {
// No-op: Not handling authorization flow
}
async codeVerifier(): Promise<string> {
throw new Error('Code verifier not supported')
}
}

View File

@ -0,0 +1,12 @@
/**
* MCP Web Extension Types
*/
export interface MCPApiResponse {
content?: Array<{
type?: string
text?: string
}>
result?: string | object
}

View File

@ -1,8 +1,11 @@
/**
* Jan Provider Authentication Service
* Shared Authentication Service
* Handles guest login and token refresh for Jan API
*/
// JAN_API_BASE is defined in vite.config.ts
declare const JAN_API_BASE: string
export interface AuthTokens {
access_token: string
expires_in: number
@ -13,7 +16,6 @@ export interface AuthResponse {
expires_in: number
}
// JAN_API_BASE is defined in vite.config.ts
const AUTH_STORAGE_KEY = 'jan_auth_tokens'
const TOKEN_EXPIRY_BUFFER = 60 * 1000 // 1 minute buffer before actual expiry
@ -184,6 +186,34 @@ export class JanAuthService {
}
}
async makeAuthenticatedRequest<T>(
url: string,
options: RequestInit = {}
): Promise<T> {
try {
const authHeader = await this.getAuthHeader()
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...authHeader,
...options.headers,
},
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`)
}
return response.json()
} catch (error) {
console.error('API request failed:', error)
throw error
}
}
logout(): void {
this.clearTokens()
}

View File

@ -0,0 +1,3 @@
export { getSharedDB } from './db'
export { JanAuthService } from './auth'
export type { AuthTokens, AuthResponse } from './auth'

View File

@ -2,7 +2,7 @@
* Web Extension Types
*/
import type { AssistantExtension, ConversationalExtension, BaseExtension, AIEngine } from '@janhq/core'
import type { AssistantExtension, ConversationalExtension, BaseExtension, AIEngine, MCPExtension } from '@janhq/core'
type ExtensionConstructorParams = ConstructorParameters<typeof BaseExtension>
@ -18,12 +18,17 @@ export interface JanProviderWebModule {
default: new (...args: ExtensionConstructorParams) => AIEngine
}
export type WebExtensionModule = AssistantWebModule | ConversationalWebModule | JanProviderWebModule
export interface MCPWebModule {
default: new (...args: ExtensionConstructorParams) => MCPExtension
}
export type WebExtensionModule = AssistantWebModule | ConversationalWebModule | JanProviderWebModule | MCPWebModule
export interface WebExtensionRegistry {
'assistant-web': () => Promise<AssistantWebModule>
'conversational-web': () => Promise<ConversationalWebModule>
'jan-provider-web': () => Promise<JanProviderWebModule>
'mcp-web': () => Promise<MCPWebModule>
}
export type WebExtensionName = keyof WebExtensionRegistry

View File

@ -14,6 +14,6 @@ export default defineConfig({
emptyOutDir: false // Don't clean the output directory
},
define: {
JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/jan/v1'),
JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'),
}
})

View File

@ -517,41 +517,41 @@ __metadata:
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=9fbce2&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=c5357d&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/e59c11201e0e95ea4ed567e3a34921c7fd2901cf32d07d13bdc8254992b6a4b51de7c1e3be633a3c2f558fe533065fa69455b065230dac87f31cb9b86649e932
checksum: 10c0/95e2ec1f1213d604730f5c9c381c80840402b00a9649039d1a9754ee3efa13e224e4ca39ea094aab5751f3f2ace1860c7640769e66b191b8c56998fd5f2ba5b9
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=9fbce2&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=c5357d&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/e59c11201e0e95ea4ed567e3a34921c7fd2901cf32d07d13bdc8254992b6a4b51de7c1e3be633a3c2f558fe533065fa69455b065230dac87f31cb9b86649e932
checksum: 10c0/95e2ec1f1213d604730f5c9c381c80840402b00a9649039d1a9754ee3efa13e224e4ca39ea094aab5751f3f2ace1860c7640769e66b191b8c56998fd5f2ba5b9
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=9fbce2&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=c5357d&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/e59c11201e0e95ea4ed567e3a34921c7fd2901cf32d07d13bdc8254992b6a4b51de7c1e3be633a3c2f558fe533065fa69455b065230dac87f31cb9b86649e932
checksum: 10c0/95e2ec1f1213d604730f5c9c381c80840402b00a9649039d1a9754ee3efa13e224e4ca39ea094aab5751f3f2ace1860c7640769e66b191b8c56998fd5f2ba5b9
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=9fbce2&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=c5357d&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/e59c11201e0e95ea4ed567e3a34921c7fd2901cf32d07d13bdc8254992b6a4b51de7c1e3be633a3c2f558fe533065fa69455b065230dac87f31cb9b86649e932
checksum: 10c0/95e2ec1f1213d604730f5c9c381c80840402b00a9649039d1a9754ee3efa13e224e4ca39ea094aab5751f3f2ace1860c7640769e66b191b8c56998fd5f2ba5b9
languageName: node
linkType: hard

View File

@ -82,12 +82,12 @@ run = [
[tasks.dev-web-app]
description = "Start web application development server (matches Makefile)"
depends = ["install"]
depends = ["build-core"]
run = "yarn dev:web-app"
[tasks.build-web-app]
description = "Build web application (matches Makefile)"
depends = ["install"]
depends = ["build-core"]
run = "yarn build:web-app"
[tasks.serve-web-app]

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
[PlatformFeature.LOCAL_INFERENCE]: isPlatformTauri(),
// MCP (Model Context Protocol) servers
[PlatformFeature.MCP_SERVERS]: isPlatformTauri(),
[PlatformFeature.MCP_SERVERS]: true,
// Local API server
[PlatformFeature.LOCAL_API_SERVER]: isPlatformTauri(),

View File

@ -23,6 +23,8 @@ import { useTranslation } from '@/i18n/react-i18next-compat'
import { useAppState } from '@/hooks/useAppState'
import { PlatformGuard } from '@/lib/platform/PlatformGuard'
import { PlatformFeature } from '@/lib/platform'
import { isPlatformTauri } from '@/lib/platform/utils'
import { MCPTool } from '@janhq/core'
// Function to mask sensitive values
const maskSensitiveValue = (value: string) => {
@ -91,12 +93,118 @@ export const Route = createFileRoute(route.settings.mcp_servers as any)({
function MCPServers() {
return (
<PlatformGuard feature={PlatformFeature.MCP_SERVERS}>
<MCPServersContent />
{isPlatformTauri() ? <MCPServersDesktop /> : <MCPServersWeb />}
</PlatformGuard>
)
}
function MCPServersContent() {
// Web version of MCP servers - simpler UI without server management
function MCPServersWeb() {
const { t } = useTranslation()
const serviceHub = useServiceHub()
const { allowAllMCPPermissions, setAllowAllMCPPermissions } = useToolApproval()
const [webMcpTools, setWebMcpTools] = useState<MCPTool[]>([])
const [webMcpServers, setWebMcpServers] = useState<string[]>([])
const [webMcpLoading, setWebMcpLoading] = useState(true)
useEffect(() => {
async function loadWebMcpData() {
setWebMcpLoading(true)
try {
const [tools, servers] = await Promise.all([
serviceHub.mcp().getTools(),
serviceHub.mcp().getConnectedServers(),
])
setWebMcpTools(tools)
setWebMcpServers(servers)
} catch (error) {
console.error('Failed to load web MCP data:', error)
setWebMcpTools([])
setWebMcpServers([])
} finally {
setWebMcpLoading(false)
}
}
loadWebMcpData()
}, [serviceHub])
return (
<div className="flex flex-col h-full">
<HeaderPage>
<h1 className="font-medium">{t('common:settings')}</h1>
</HeaderPage>
<div className="flex h-full w-full">
<SettingsMenu />
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">
<div className="flex flex-col justify-between gap-4 gap-y-3 w-full">
<Card
header={
<div className="flex flex-col mb-4">
<h1 className="text-main-view-fg font-medium text-base">
{t('mcp-servers:title')}
</h1>
<p className="text-sm text-main-view-fg/70 mt-1">
MCP tools are automatically available in your chat sessions
</p>
</div>
}
>
<CardItem
title={t('mcp-servers:allowPermissions')}
description={t('mcp-servers:allowPermissionsDesc')}
actions={
<div className="flex-shrink-0 ml-4">
<Switch
checked={allowAllMCPPermissions}
onCheckedChange={setAllowAllMCPPermissions}
/>
</div>
}
/>
</Card>
<Card>
<CardItem
title="MCP Service Status"
description={
webMcpLoading
? "Loading MCP service status..."
: webMcpServers.length > 0
? `Connected to ${webMcpServers.join(', ')}. ${webMcpTools.length} tools available.`
: "MCP service not connected"
}
descriptionOutside={
webMcpTools.length > 0 && !webMcpLoading && (
<div className="mt-2">
<h4 className="text-sm font-medium text-main-view-fg/80 mb-2">Available Tools:</h4>
<div className="grid grid-cols-1 gap-2">
{webMcpTools.map((tool) => (
<div key={tool.name} className="flex items-start gap-2 p-2 bg-main-view-fg/5 rounded">
<div className="flex-1">
<div className="font-medium text-sm">{tool.name}</div>
<div className="text-xs text-main-view-fg/70">{tool.description}</div>
{tool.server && (
<div className="text-xs text-main-view-fg/50 mt-1">Server: {tool.server}</div>
)}
</div>
</div>
))}
</div>
</div>
)
}
/>
</Card>
</div>
</div>
</div>
</div>
)
}
// Desktop version of MCP servers - full server management capabilities
function MCPServersDesktop() {
const { t } = useTranslation()
const serviceHub = useServiceHub()
const {

View File

@ -157,6 +157,7 @@ class PlatformServiceHub implements ServiceHub {
windowModule,
deepLinkModule,
providersModule,
mcpModule,
] = await Promise.all([
import('./theme/web'),
import('./app/web'),
@ -167,6 +168,7 @@ class PlatformServiceHub implements ServiceHub {
import('./window/web'),
import('./deeplink/web'),
import('./providers/web'),
import('./mcp/web'),
])
this.themeService = new themeModule.WebThemeService()
@ -178,6 +180,7 @@ class PlatformServiceHub implements ServiceHub {
this.windowService = new windowModule.WebWindowService()
this.deepLinkService = new deepLinkModule.WebDeepLinkService()
this.providersService = new providersModule.WebProvidersService()
this.mcpService = new mcpModule.WebMCPService()
}
this.initialized = true

View File

@ -2,9 +2,9 @@
* Default MCP Service - Generic implementation with minimal returns
*/
import { MCPTool } from '@/types/completion'
import { MCPTool, MCPToolCallResult } from '@janhq/core'
import type { MCPServerConfig } from '@/hooks/useMCPServers'
import type { MCPService, MCPConfig, ToolCallResult, ToolCallWithCancellationResult } from './types'
import type { MCPService, MCPConfig, ToolCallWithCancellationResult } from './types'
export class DefaultMCPService implements MCPService {
async updateMCPConfig(configs: string): Promise<void> {
@ -28,7 +28,7 @@ export class DefaultMCPService implements MCPService {
return []
}
async callTool(args: { toolName: string; arguments: object }): Promise<ToolCallResult> {
async callTool(args: { toolName: string; arguments: object }): Promise<MCPToolCallResult> {
console.log('callTool called with args:', args)
return {
error: '',

View File

@ -2,20 +2,15 @@
* MCP Service Types
*/
import { MCPTool } from '@/types/completion'
import { MCPTool, MCPToolCallResult } from '@janhq/core'
import type { MCPServerConfig, MCPServers } from '@/hooks/useMCPServers'
export interface MCPConfig {
mcpServers?: MCPServers
}
export interface ToolCallResult {
error: string
content: { text: string }[]
}
export interface ToolCallWithCancellationResult {
promise: Promise<ToolCallResult>
promise: Promise<MCPToolCallResult>
cancel: () => Promise<void>
token: string
}
@ -26,7 +21,7 @@ export interface MCPService {
getMCPConfig(): Promise<MCPConfig>
getTools(): Promise<MCPTool[]>
getConnectedServers(): Promise<string[]>
callTool(args: { toolName: string; arguments: object }): Promise<ToolCallResult>
callTool(args: { toolName: string; arguments: object }): Promise<MCPToolCallResult>
callToolWithCancellation(args: {
toolName: string
arguments: object

View File

@ -0,0 +1,279 @@
/**
* Web MCP Service - Implementation for web platform
* Uses the MCP extension through ExtensionManager
* Follows OpenAI function calling standards
*/
import type { MCPServerConfig } from '@/hooks/useMCPServers'
import type { MCPService, MCPConfig, ToolCallWithCancellationResult } from './types'
import { ExtensionManager } from '@/lib/extension'
import { ExtensionTypeEnum, MCPExtension, MCPTool, MCPToolCallResult } from '@janhq/core'
export class WebMCPService implements MCPService {
private abortControllers: Map<string, AbortController> = new Map()
private extensionCache: MCPExtension | null = null
private cacheTimestamp = 0
private readonly CACHE_TTL = 5000 // 5 seconds
private getMCPExtension(): MCPExtension | null {
const now = Date.now()
if (this.extensionCache && (now - this.cacheTimestamp) < this.CACHE_TTL) {
return this.extensionCache
}
this.extensionCache = ExtensionManager.getInstance().get<MCPExtension>(
ExtensionTypeEnum.MCP
) || null
this.cacheTimestamp = now
return this.extensionCache
}
private invalidateCache(): void {
this.extensionCache = null
this.cacheTimestamp = 0
}
async updateMCPConfig(configs: string): Promise<void> {
if (!configs || typeof configs !== 'string') {
throw new Error('Invalid MCP configuration provided')
}
// For web platform, configuration is handled by the remote API server
// Invalidate cache to ensure fresh extension retrieval
this.invalidateCache()
}
async restartMCPServers(): Promise<void> {
// For web platform, servers are managed remotely
// This triggers a refresh of available tools
this.invalidateCache()
const extension = this.getMCPExtension()
if (extension) {
try {
await extension.refreshTools()
} catch (error) {
throw new Error(`Failed to restart MCP servers: ${error instanceof Error ? error.message : String(error)}`)
}
}
}
async getMCPConfig(): Promise<MCPConfig> {
// Return empty config since web platform doesn't manage local MCP servers
return {}
}
async getTools(): Promise<MCPTool[]> {
const extension = this.getMCPExtension()
if (!extension) {
return []
}
try {
return await extension.getTools()
} catch (error) {
console.error('Failed to get MCP tools:', error)
return []
}
}
async getConnectedServers(): Promise<string[]> {
const extension = this.getMCPExtension()
if (!extension) {
return []
}
try {
return await extension.getConnectedServers()
} catch (error) {
console.error('Failed to get connected servers:', error)
return []
}
}
async callTool(args: { toolName: string; arguments: object }): Promise<MCPToolCallResult> {
// Validate input parameters
if (!args.toolName || typeof args.toolName !== 'string') {
return {
error: 'Invalid tool name provided',
content: [{ type: 'text', text: 'Tool name must be a non-empty string' }]
}
}
const extension = this.getMCPExtension()
if (!extension) {
return {
error: 'MCP extension not available',
content: [{ type: 'text', text: 'MCP service is not available' }]
}
}
try {
const result = await extension.callTool(args.toolName, args.arguments as Record<string, unknown>)
// Ensure OpenAI-compliant response format
if (!result.content || !Array.isArray(result.content)) {
return {
error: 'Invalid tool response format',
content: [{ type: 'text', text: 'Tool returned invalid response format' }]
}
}
return {
error: result.error || '',
content: result.content.map(item => ({
type: item.type || 'text',
text: item.text || JSON.stringify(item)
}))
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
error: errorMessage,
content: [{ type: 'text', text: `Tool execution failed: ${errorMessage}` }]
}
}
}
callToolWithCancellation(args: {
toolName: string
arguments: object
cancellationToken?: string
}): ToolCallWithCancellationResult {
// Validate input parameters
if (!args.toolName || typeof args.toolName !== 'string') {
const errorResult: MCPToolCallResult = {
error: 'Invalid tool name provided',
content: [{ type: 'text', text: 'Tool name must be a non-empty string' }]
}
return {
promise: Promise.resolve(errorResult),
cancel: async () => {}, // No-op for failed validation
token: 'invalid'
}
}
const token = args.cancellationToken || this.generateCancellationToken()
const abortController = new AbortController()
this.abortControllers.set(token, abortController)
const promise = this.callToolWithAbort(args, abortController.signal)
.finally(() => {
this.abortControllers.delete(token)
})
return {
promise,
cancel: async () => {
const controller = this.abortControllers.get(token)
if (controller && !controller.signal.aborted) {
controller.abort()
}
this.abortControllers.delete(token)
},
token
}
}
private async callToolWithAbort(
args: { toolName: string; arguments: object },
signal: AbortSignal
): Promise<MCPToolCallResult> {
// Check if already aborted
if (signal.aborted) {
return {
error: 'Tool call was cancelled',
content: [{ type: 'text', text: 'Tool call was cancelled by user' }]
}
}
const extension = this.getMCPExtension()
if (!extension) {
return {
error: 'MCP extension not available',
content: [{ type: 'text', text: 'MCP service is not available' }]
}
}
return new Promise((resolve) => {
const abortHandler = () => {
resolve({
error: 'Tool call was cancelled',
content: [{ type: 'text', text: 'Tool call was cancelled by user' }]
})
}
signal.addEventListener('abort', abortHandler, { once: true })
extension.callTool(args.toolName, args.arguments as Record<string, unknown>)
.then(result => {
if (!signal.aborted) {
if (!result.content || !Array.isArray(result.content)) {
resolve({
error: 'Invalid tool response format',
content: [{ type: 'text', text: 'Tool returned invalid response format' }]
})
return
}
resolve({
error: result.error || '',
content: result.content.map(item => ({
type: item.type || 'text',
text: item.text || JSON.stringify(item)
}))
})
}
})
.catch(error => {
if (!signal.aborted) {
const errorMessage = error instanceof Error ? error.message : String(error)
resolve({
error: errorMessage,
content: [{ type: 'text', text: `Tool execution failed: ${errorMessage}` }]
})
}
})
.finally(() => {
signal.removeEventListener('abort', abortHandler)
})
})
}
async cancelToolCall(cancellationToken: string): Promise<void> {
const controller = this.abortControllers.get(cancellationToken)
if (controller) {
controller.abort()
this.abortControllers.delete(cancellationToken)
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async activateMCPServer(name: string, _config: MCPServerConfig): Promise<void> {
// For web platform, server activation is handled remotely
this.invalidateCache()
const extension = this.getMCPExtension()
if (extension) {
try {
await extension.refreshTools()
} catch (error) {
throw new Error(`Failed to activate MCP server ${name}: ${error instanceof Error ? error.message : String(error)}`)
}
}
}
async deactivateMCPServer(name: string): Promise<void> {
// For web platform, server deactivation is handled remotely
this.invalidateCache()
const extension = this.getMCPExtension()
if (extension) {
try {
await extension.refreshTools()
} catch (error) {
throw new Error(`Failed to deactivate MCP server ${name}: ${error instanceof Error ? error.message : String(error)}`)
}
}
}
private generateCancellationToken(): string {
return `mcp_cancel_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
}
}

471
yarn.lock
View File

@ -3464,6 +3464,7 @@ __metadata:
resolution: "@jan/extensions-web@workspace:extensions-web"
dependencies:
"@janhq/core": "workspace:*"
"@modelcontextprotocol/sdk": "npm:^1.17.5"
typescript: "npm:^5.3.3"
vite: "npm:^5.0.0"
vitest: "npm:^2.0.0"
@ -3590,6 +3591,7 @@ __metadata:
react-textarea-autosize: "npm:^8.5.9"
rehype-katex: "npm:^7.0.1"
rehype-raw: "npm:^7.0.0"
remark-breaks: "npm:^4.0.0"
remark-emoji: "npm:^5.0.1"
remark-gfm: "npm:^4.0.1"
remark-math: "npm:^6.0.0"
@ -3721,6 +3723,26 @@ __metadata:
languageName: node
linkType: hard
"@modelcontextprotocol/sdk@npm:^1.17.5":
version: 1.17.5
resolution: "@modelcontextprotocol/sdk@npm:1.17.5"
dependencies:
ajv: "npm:^6.12.6"
content-type: "npm:^1.0.5"
cors: "npm:^2.8.5"
cross-spawn: "npm:^7.0.5"
eventsource: "npm:^3.0.2"
eventsource-parser: "npm:^3.0.0"
express: "npm:^5.0.1"
express-rate-limit: "npm:^7.5.0"
pkce-challenge: "npm:^5.0.0"
raw-body: "npm:^3.0.0"
zod: "npm:^3.23.8"
zod-to-json-schema: "npm:^3.24.1"
checksum: 10c0/182b92b5e7c07da428fd23c6de22021c4f9a91f799c02a8ef15def07e4f9361d0fc22303548658fec2a700623535fd44a9dc4d010fb5d803a8f80e3c6c64a45e
languageName: node
linkType: hard
"@napi-rs/wasm-runtime@npm:^0.2.4":
version: 0.2.7
resolution: "@napi-rs/wasm-runtime@npm:0.2.7"
@ -8356,6 +8378,16 @@ __metadata:
languageName: node
linkType: hard
"accepts@npm:^2.0.0":
version: 2.0.0
resolution: "accepts@npm:2.0.0"
dependencies:
mime-types: "npm:^3.0.0"
negotiator: "npm:^1.0.0"
checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef
languageName: node
linkType: hard
"acorn-jsx@npm:^5.3.2":
version: 5.3.2
resolution: "acorn-jsx@npm:5.3.2"
@ -8430,7 +8462,7 @@ __metadata:
languageName: node
linkType: hard
"ajv@npm:^6.12.3, ajv@npm:^6.12.4":
"ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.6":
version: 6.12.6
resolution: "ajv@npm:6.12.6"
dependencies:
@ -8967,6 +8999,23 @@ __metadata:
languageName: node
linkType: hard
"body-parser@npm:^2.2.0":
version: 2.2.0
resolution: "body-parser@npm:2.2.0"
dependencies:
bytes: "npm:^3.1.2"
content-type: "npm:^1.0.5"
debug: "npm:^4.4.0"
http-errors: "npm:^2.0.0"
iconv-lite: "npm:^0.6.3"
on-finished: "npm:^2.4.1"
qs: "npm:^6.14.0"
raw-body: "npm:^3.0.0"
type-is: "npm:^2.0.0"
checksum: 10c0/a9ded39e71ac9668e2211afa72e82ff86cc5ef94de1250b7d1ba9cc299e4150408aaa5f1e8b03dd4578472a3ce6d1caa2a23b27a6c18e526e48b4595174c116c
languageName: node
linkType: hard
"bowser@npm:^2.11.0":
version: 2.11.0
resolution: "bowser@npm:2.11.0"
@ -9221,7 +9270,7 @@ __metadata:
languageName: node
linkType: hard
"bytes@npm:3.1.2":
"bytes@npm:3.1.2, bytes@npm:^3.1.2":
version: 3.1.2
resolution: "bytes@npm:3.1.2"
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
@ -9798,6 +9847,22 @@ __metadata:
languageName: node
linkType: hard
"content-disposition@npm:^1.0.0":
version: 1.0.0
resolution: "content-disposition@npm:1.0.0"
dependencies:
safe-buffer: "npm:5.2.1"
checksum: 10c0/c7b1ba0cea2829da0352ebc1b7f14787c73884bc707c8bc2271d9e3bf447b372270d09f5d3980dc5037c749ceef56b9a13fccd0b0001c87c3f12579967e4dd27
languageName: node
linkType: hard
"content-type@npm:^1.0.5":
version: 1.0.5
resolution: "content-type@npm:1.0.5"
checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af
languageName: node
linkType: hard
"convert-source-map@npm:^2.0.0":
version: 2.0.0
resolution: "convert-source-map@npm:2.0.0"
@ -9805,6 +9870,20 @@ __metadata:
languageName: node
linkType: hard
"cookie-signature@npm:^1.2.1":
version: 1.2.2
resolution: "cookie-signature@npm:1.2.2"
checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6
languageName: node
linkType: hard
"cookie@npm:^0.7.1":
version: 0.7.2
resolution: "cookie@npm:0.7.2"
checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2
languageName: node
linkType: hard
"copy-descriptor@npm:^0.1.0":
version: 0.1.1
resolution: "copy-descriptor@npm:0.1.1"
@ -9849,6 +9928,16 @@ __metadata:
languageName: node
linkType: hard
"cors@npm:^2.8.5":
version: 2.8.5
resolution: "cors@npm:2.8.5"
dependencies:
object-assign: "npm:^4"
vary: "npm:^1"
checksum: 10c0/373702b7999409922da80de4a61938aabba6929aea5b6fd9096fefb9e8342f626c0ebd7507b0e8b0b311380744cc985f27edebc0a26e0ddb784b54e1085de761
languageName: node
linkType: hard
"cpx@npm:^1.5.0":
version: 1.5.0
resolution: "cpx@npm:1.5.0"
@ -9938,7 +10027,7 @@ __metadata:
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6":
"cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
dependencies:
@ -10089,7 +10178,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:^4.3.7, debug@npm:^4.4.1":
"debug@npm:^4.3.5, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.1":
version: 4.4.1
resolution: "debug@npm:4.4.1"
dependencies:
@ -10225,6 +10314,13 @@ __metadata:
languageName: node
linkType: hard
"depd@npm:2.0.0, depd@npm:^2.0.0":
version: 2.0.0
resolution: "depd@npm:2.0.0"
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
languageName: node
linkType: hard
"dequal@npm:^2.0.0, dequal@npm:^2.0.3":
version: 2.0.3
resolution: "dequal@npm:2.0.3"
@ -10376,6 +10472,13 @@ __metadata:
languageName: node
linkType: hard
"ee-first@npm:1.1.1":
version: 1.1.1
resolution: "ee-first@npm:1.1.1"
checksum: 10c0/b5bb125ee93161bc16bfe6e56c6b04de5ad2aa44234d8f644813cc95d861a6910903132b05093706de2b706599367c4130eb6d170f6b46895686b95f87d017b7
languageName: node
linkType: hard
"ejs@npm:^3.1.6":
version: 3.1.10
resolution: "ejs@npm:3.1.10"
@ -10455,6 +10558,13 @@ __metadata:
languageName: node
linkType: hard
"encodeurl@npm:^2.0.0":
version: 2.0.0
resolution: "encodeurl@npm:2.0.0"
checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb
languageName: node
linkType: hard
"encoding@npm:^0.1.13":
version: 0.1.13
resolution: "encoding@npm:0.1.13"
@ -10790,6 +10900,13 @@ __metadata:
languageName: node
linkType: hard
"escape-html@npm:^1.0.3":
version: 1.0.3
resolution: "escape-html@npm:1.0.3"
checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3
languageName: node
linkType: hard
"escape-string-regexp@npm:^4.0.0":
version: 4.0.0
resolution: "escape-string-regexp@npm:4.0.0"
@ -11048,6 +11165,13 @@ __metadata:
languageName: node
linkType: hard
"etag@npm:^1.8.1":
version: 1.8.1
resolution: "etag@npm:1.8.1"
checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84
languageName: node
linkType: hard
"event-target-shim@npm:^5.0.0":
version: 5.0.1
resolution: "event-target-shim@npm:5.0.1"
@ -11062,6 +11186,22 @@ __metadata:
languageName: node
linkType: hard
"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.1":
version: 3.0.6
resolution: "eventsource-parser@npm:3.0.6"
checksum: 10c0/70b8ccec7dac767ef2eca43f355e0979e70415701691382a042a2df8d6a68da6c2fca35363669821f3da876d29c02abe9b232964637c1b6635c940df05ada78a
languageName: node
linkType: hard
"eventsource@npm:^3.0.2":
version: 3.0.7
resolution: "eventsource@npm:3.0.7"
dependencies:
eventsource-parser: "npm:^3.0.1"
checksum: 10c0/c48a73c38f300e33e9f11375d4ee969f25cbb0519608a12378a38068055ae8b55b6e0e8a49c3f91c784068434efe1d9f01eb49b6315b04b0da9157879ce2f67d
languageName: node
linkType: hard
"evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3":
version: 1.0.3
resolution: "evp_bytestokey@npm:1.0.3"
@ -11144,6 +11284,50 @@ __metadata:
languageName: node
linkType: hard
"express-rate-limit@npm:^7.5.0":
version: 7.5.1
resolution: "express-rate-limit@npm:7.5.1"
peerDependencies:
express: ">= 4.11"
checksum: 10c0/b07de84d700a2c07c4bf2f040e7558ed5a1f660f03ed5f30bf8ff7b51e98ba7a85215640e70fc48cbbb9151066ea51239d9a1b41febc9b84d98c7915b0186161
languageName: node
linkType: hard
"express@npm:^5.0.1":
version: 5.1.0
resolution: "express@npm:5.1.0"
dependencies:
accepts: "npm:^2.0.0"
body-parser: "npm:^2.2.0"
content-disposition: "npm:^1.0.0"
content-type: "npm:^1.0.5"
cookie: "npm:^0.7.1"
cookie-signature: "npm:^1.2.1"
debug: "npm:^4.4.0"
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
etag: "npm:^1.8.1"
finalhandler: "npm:^2.1.0"
fresh: "npm:^2.0.0"
http-errors: "npm:^2.0.0"
merge-descriptors: "npm:^2.0.0"
mime-types: "npm:^3.0.0"
on-finished: "npm:^2.4.1"
once: "npm:^1.4.0"
parseurl: "npm:^1.3.3"
proxy-addr: "npm:^2.0.7"
qs: "npm:^6.14.0"
range-parser: "npm:^1.2.1"
router: "npm:^2.2.0"
send: "npm:^1.1.0"
serve-static: "npm:^2.2.0"
statuses: "npm:^2.0.1"
type-is: "npm:^2.0.1"
vary: "npm:^1.1.2"
checksum: 10c0/80ce7c53c5f56887d759b94c3f2283e2e51066c98d4b72a4cc1338e832b77f1e54f30d0239cc10815a0f849bdb753e6a284d2fa48d4ab56faf9c501f55d751d6
languageName: node
linkType: hard
"extend-shallow@npm:^2.0.1":
version: 2.0.1
resolution: "extend-shallow@npm:2.0.1"
@ -11425,6 +11609,20 @@ __metadata:
languageName: node
linkType: hard
"finalhandler@npm:^2.1.0":
version: 2.1.0
resolution: "finalhandler@npm:2.1.0"
dependencies:
debug: "npm:^4.4.0"
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
on-finished: "npm:^2.4.1"
parseurl: "npm:^1.3.3"
statuses: "npm:^2.0.1"
checksum: 10c0/da0bbca6d03873472ee890564eb2183f4ed377f25f3628a0fc9d16dac40bed7b150a0d82ebb77356e4c6d97d2796ad2dba22948b951dddee2c8768b0d1b9fb1f
languageName: node
linkType: hard
"find-index@npm:^0.1.1":
version: 0.1.1
resolution: "find-index@npm:0.1.1"
@ -11591,6 +11789,13 @@ __metadata:
languageName: node
linkType: hard
"forwarded@npm:0.2.0":
version: 0.2.0
resolution: "forwarded@npm:0.2.0"
checksum: 10c0/9b67c3fac86acdbc9ae47ba1ddd5f2f81526fa4c8226863ede5600a3f7c7416ef451f6f1e240a3cc32d0fd79fcfe6beb08fd0da454f360032bde70bf80afbb33
languageName: node
linkType: hard
"fragment-cache@npm:^0.2.1":
version: 0.2.1
resolution: "fragment-cache@npm:0.2.1"
@ -11644,6 +11849,13 @@ __metadata:
languageName: node
linkType: hard
"fresh@npm:^2.0.0":
version: 2.0.0
resolution: "fresh@npm:2.0.0"
checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc
languageName: node
linkType: hard
"fs-extra@npm:^11.2.0":
version: 11.3.0
resolution: "fs-extra@npm:11.3.0"
@ -12593,6 +12805,19 @@ __metadata:
languageName: node
linkType: hard
"http-errors@npm:2.0.0, http-errors@npm:^2.0.0":
version: 2.0.0
resolution: "http-errors@npm:2.0.0"
dependencies:
depd: "npm:2.0.0"
inherits: "npm:2.0.4"
setprototypeof: "npm:1.2.0"
statuses: "npm:2.0.1"
toidentifier: "npm:1.0.1"
checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19
languageName: node
linkType: hard
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2":
version: 7.0.2
resolution: "http-proxy-agent@npm:7.0.2"
@ -12670,7 +12895,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@ -12679,6 +12904,15 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:0.7.0":
version: 0.7.0
resolution: "iconv-lite@npm:0.7.0"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
languageName: node
linkType: hard
"idb@npm:^7.0.1":
version: 7.1.1
resolution: "idb@npm:7.1.1"
@ -12752,7 +12986,7 @@ __metadata:
languageName: node
linkType: hard
"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4":
"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3, inherits@npm:~2.0.4":
version: 2.0.4
resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
@ -12808,6 +13042,13 @@ __metadata:
languageName: node
linkType: hard
"ipaddr.js@npm:1.9.1":
version: 1.9.1
resolution: "ipaddr.js@npm:1.9.1"
checksum: 10c0/0486e775047971d3fdb5fb4f063829bac45af299ae0b82dcf3afa2145338e08290563a2a70f34b732d795ecc8311902e541a8530eeb30d75860a78ff4e94ce2a
languageName: node
linkType: hard
"is-accessor-descriptor@npm:^1.0.1":
version: 1.0.1
resolution: "is-accessor-descriptor@npm:1.0.1"
@ -13276,6 +13517,13 @@ __metadata:
languageName: node
linkType: hard
"is-promise@npm:^4.0.0":
version: 4.0.0
resolution: "is-promise@npm:4.0.0"
checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503
languageName: node
linkType: hard
"is-regex@npm:^1.2.1":
version: 1.2.1
resolution: "is-regex@npm:1.2.1"
@ -13625,6 +13873,7 @@ __metadata:
cpx: "npm:^1.5.0"
cross-env: "npm:^7.0.3"
husky: "npm:^9.1.5"
jsdom: "npm:^26.1.0"
rimraf: "npm:^3.0.2"
run-script-os: "npm:^1.1.6"
tar: "npm:^4.4.19"
@ -14549,6 +14798,16 @@ __metadata:
languageName: node
linkType: hard
"mdast-util-newline-to-break@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-newline-to-break@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
mdast-util-find-and-replace: "npm:^3.0.0"
checksum: 10c0/756a5660b0a821e0d6d6a0b2d9b13ac32e41cc028c485a91bccf6300977e2557236c6cc93dbd55c68b785f1ed6eae69209a4ffe182533cd1cdfda369021bebd2
languageName: node
linkType: hard
"mdast-util-phrasing@npm:^4.0.0":
version: 4.1.0
resolution: "mdast-util-phrasing@npm:4.1.0"
@ -14602,6 +14861,20 @@ __metadata:
languageName: node
linkType: hard
"media-typer@npm:^1.1.0":
version: 1.1.0
resolution: "media-typer@npm:1.1.0"
checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9
languageName: node
linkType: hard
"merge-descriptors@npm:^2.0.0":
version: 2.0.0
resolution: "merge-descriptors@npm:2.0.0"
checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3
languageName: node
linkType: hard
"merge-stream@npm:^2.0.0":
version: 2.0.0
resolution: "merge-stream@npm:2.0.0"
@ -15031,7 +15304,7 @@ __metadata:
languageName: node
linkType: hard
"mime-db@npm:>= 1.43.0 < 2":
"mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.54.0":
version: 1.54.0
resolution: "mime-db@npm:1.54.0"
checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284
@ -15063,6 +15336,15 @@ __metadata:
languageName: node
linkType: hard
"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1":
version: 3.0.1
resolution: "mime-types@npm:3.0.1"
dependencies:
mime-db: "npm:^1.54.0"
checksum: 10c0/bd8c20d3694548089cf229016124f8f40e6a60bbb600161ae13e45f793a2d5bb40f96bbc61f275836696179c77c1d6bf4967b2a75e0a8ad40fe31f4ed5be4da5
languageName: node
linkType: hard
"mimic-fn@npm:^2.1.0":
version: 2.1.0
resolution: "mimic-fn@npm:2.1.0"
@ -15803,7 +16085,7 @@ __metadata:
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
"object-assign@npm:^4, object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414
@ -15887,6 +16169,15 @@ __metadata:
languageName: node
linkType: hard
"on-finished@npm:^2.4.1":
version: 2.4.1
resolution: "on-finished@npm:2.4.1"
dependencies:
ee-first: "npm:1.1.1"
checksum: 10c0/46fb11b9063782f2d9968863d9cbba33d77aa13c17f895f56129c274318b86500b22af3a160fe9995aa41317efcd22941b6eba747f718ced08d9a73afdb087b4
languageName: node
linkType: hard
"on-headers@npm:~1.1.0":
version: 1.1.0
resolution: "on-headers@npm:1.1.0"
@ -16171,6 +16462,13 @@ __metadata:
languageName: node
linkType: hard
"parseurl@npm:^1.3.3":
version: 1.3.3
resolution: "parseurl@npm:1.3.3"
checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5
languageName: node
linkType: hard
"pascalcase@npm:^0.1.1":
version: 0.1.1
resolution: "pascalcase@npm:0.1.1"
@ -16247,6 +16545,13 @@ __metadata:
languageName: node
linkType: hard
"path-to-regexp@npm:^8.0.0":
version: 8.3.0
resolution: "path-to-regexp@npm:8.3.0"
checksum: 10c0/ee1544a73a3f294a97a4c663b0ce71bbf1621d732d80c9c9ed201b3e911a86cb628ebad691b9d40f40a3742fe22011e5a059d8eed2cf63ec2cb94f6fb4efe67c
languageName: node
linkType: hard
"pathe@npm:^1.1.2":
version: 1.1.2
resolution: "pathe@npm:1.1.2"
@ -16324,6 +16629,13 @@ __metadata:
languageName: node
linkType: hard
"pkce-challenge@npm:^5.0.0":
version: 5.0.0
resolution: "pkce-challenge@npm:5.0.0"
checksum: 10c0/c6706d627fdbb6f22bf8cc5d60d96d6b6a7bb481399b336a3d3f4e9bfba3e167a2c32f8ec0b5e74be686a0ba3bcc9894865d4c2dd1b91cea4c05dba1f28602c3
languageName: node
linkType: hard
"pkg-dir@npm:^5.0.0":
version: 5.0.0
resolution: "pkg-dir@npm:5.0.0"
@ -16575,6 +16887,16 @@ __metadata:
languageName: node
linkType: hard
"proxy-addr@npm:^2.0.7":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"
dependencies:
forwarded: "npm:0.2.0"
ipaddr.js: "npm:1.9.1"
checksum: 10c0/c3eed999781a35f7fd935f398b6d8920b6fb00bbc14287bc6de78128ccc1a02c89b95b56742bf7cf0362cc333c61d138532049c7dedc7a328ef13343eff81210
languageName: node
linkType: hard
"psl@npm:^1.1.28":
version: 1.15.0
resolution: "psl@npm:1.15.0"
@ -16621,7 +16943,7 @@ __metadata:
languageName: node
linkType: hard
"qs@npm:^6.12.3":
"qs@npm:^6.12.3, qs@npm:^6.14.0":
version: 6.14.0
resolution: "qs@npm:6.14.0"
dependencies:
@ -16688,6 +17010,25 @@ __metadata:
languageName: node
linkType: hard
"range-parser@npm:^1.2.1":
version: 1.2.1
resolution: "range-parser@npm:1.2.1"
checksum: 10c0/96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0
languageName: node
linkType: hard
"raw-body@npm:^3.0.0":
version: 3.0.1
resolution: "raw-body@npm:3.0.1"
dependencies:
bytes: "npm:3.1.2"
http-errors: "npm:2.0.0"
iconv-lite: "npm:0.7.0"
unpipe: "npm:1.0.0"
checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a
languageName: node
linkType: hard
"rc@npm:^1.0.1, rc@npm:^1.1.6":
version: 1.2.8
resolution: "rc@npm:1.2.8"
@ -17295,6 +17636,17 @@ __metadata:
languageName: node
linkType: hard
"remark-breaks@npm:^4.0.0":
version: 4.0.0
resolution: "remark-breaks@npm:4.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
mdast-util-newline-to-break: "npm:^2.0.0"
unified: "npm:^11.0.0"
checksum: 10c0/d7b319a7993b54c5d574e9255080c5de68cfa24f993873b0ee296af13f478521c41d4b7ae0fc14b4607ea70c8f6967e998ab7a467de13139141e66a1a34cb6be
languageName: node
linkType: hard
"remark-emoji@npm:^5.0.1":
version: 5.0.1
resolution: "remark-emoji@npm:5.0.1"
@ -17863,6 +18215,19 @@ __metadata:
languageName: node
linkType: hard
"router@npm:^2.2.0":
version: 2.2.0
resolution: "router@npm:2.2.0"
dependencies:
debug: "npm:^4.4.0"
depd: "npm:^2.0.0"
is-promise: "npm:^4.0.0"
parseurl: "npm:^1.3.3"
path-to-regexp: "npm:^8.0.0"
checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.8.0":
version: 0.8.0
resolution: "rrweb-cssom@npm:0.8.0"
@ -18019,6 +18384,25 @@ __metadata:
languageName: node
linkType: hard
"send@npm:^1.1.0, send@npm:^1.2.0":
version: 1.2.0
resolution: "send@npm:1.2.0"
dependencies:
debug: "npm:^4.3.5"
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
etag: "npm:^1.8.1"
fresh: "npm:^2.0.0"
http-errors: "npm:^2.0.0"
mime-types: "npm:^3.0.1"
ms: "npm:^2.1.3"
on-finished: "npm:^2.4.1"
range-parser: "npm:^1.2.1"
statuses: "npm:^2.0.1"
checksum: 10c0/531bcfb5616948d3468d95a1fd0adaeb0c20818ba4a500f439b800ca2117971489e02074ce32796fd64a6772ea3e7235fe0583d8241dbd37a053dc3378eff9a5
languageName: node
linkType: hard
"serialize-javascript@npm:^6.0.1":
version: 6.0.2
resolution: "serialize-javascript@npm:6.0.2"
@ -18059,6 +18443,18 @@ __metadata:
languageName: node
linkType: hard
"serve-static@npm:^2.2.0":
version: 2.2.0
resolution: "serve-static@npm:2.2.0"
dependencies:
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
parseurl: "npm:^1.3.3"
send: "npm:^1.2.0"
checksum: 10c0/30e2ed1dbff1984836cfd0c65abf5d3f3f83bcd696c99d2d3c97edbd4e2a3ff4d3f87108a7d713640d290a7b6fe6c15ddcbc61165ab2eaad48ea8d3b52c7f913
languageName: node
linkType: hard
"serve@npm:^14.2.4":
version: 14.2.5
resolution: "serve@npm:14.2.5"
@ -18136,6 +18532,13 @@ __metadata:
languageName: node
linkType: hard
"setprototypeof@npm:1.2.0":
version: 1.2.0
resolution: "setprototypeof@npm:1.2.0"
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
languageName: node
linkType: hard
"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8":
version: 2.4.11
resolution: "sha.js@npm:2.4.11"
@ -18574,6 +18977,20 @@ __metadata:
languageName: node
linkType: hard
"statuses@npm:2.0.1":
version: 2.0.1
resolution: "statuses@npm:2.0.1"
checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0
languageName: node
linkType: hard
"statuses@npm:^2.0.1":
version: 2.0.2
resolution: "statuses@npm:2.0.2"
checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f
languageName: node
linkType: hard
"std-env@npm:^3.8.0, std-env@npm:^3.9.0":
version: 3.9.0
resolution: "std-env@npm:3.9.0"
@ -19154,6 +19571,13 @@ __metadata:
languageName: node
linkType: hard
"toidentifier@npm:1.0.1":
version: 1.0.1
resolution: "toidentifier@npm:1.0.1"
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
languageName: node
linkType: hard
"token.js@npm:token.js-fork@0.7.23":
version: 0.7.23
resolution: "token.js-fork@npm:0.7.23"
@ -19391,6 +19815,17 @@ __metadata:
languageName: node
linkType: hard
"type-is@npm:^2.0.0, type-is@npm:^2.0.1":
version: 2.0.1
resolution: "type-is@npm:2.0.1"
dependencies:
content-type: "npm:^1.0.5"
media-typer: "npm:^1.1.0"
mime-types: "npm:^3.0.0"
checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99
languageName: node
linkType: hard
"typed-array-buffer@npm:^1.0.3":
version: 1.0.3
resolution: "typed-array-buffer@npm:1.0.3"
@ -19736,6 +20171,13 @@ __metadata:
languageName: node
linkType: hard
"unpipe@npm:1.0.0":
version: 1.0.0
resolution: "unpipe@npm:1.0.0"
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c
languageName: node
linkType: hard
"unplugin@npm:^2.1.2":
version: 2.3.2
resolution: "unplugin@npm:2.3.2"
@ -20004,7 +20446,7 @@ __metadata:
languageName: node
linkType: hard
"vary@npm:~1.1.2":
"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2":
version: 1.1.2
resolution: "vary@npm:1.1.2"
checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f
@ -20941,6 +21383,15 @@ __metadata:
languageName: node
linkType: hard
"zod-to-json-schema@npm:^3.24.1":
version: 3.24.6
resolution: "zod-to-json-schema@npm:3.24.6"
peerDependencies:
zod: ^3.24.1
checksum: 10c0/b907ab6d057100bd25a37e5545bf5f0efa5902cd84d3c3ec05c2e51541431a47bd9bf1e5e151a244273409b45f5986d55b26e5d207f98abc5200702f733eb368
languageName: node
linkType: hard
"zod@npm:^3.23.8":
version: 3.24.2
resolution: "zod@npm:3.24.2"