diff --git a/.changeset/add-mcp-client-support.md b/.changeset/add-mcp-client-support.md new file mode 100644 index 00000000..7ff6c9e2 --- /dev/null +++ b/.changeset/add-mcp-client-support.md @@ -0,0 +1,5 @@ +--- +"chat": minor +--- + +Add MCP (Model Context Protocol) client support for tool discovery and invocation from remote MCP servers. Configure via `mcpServers` in `ChatConfig` and access tools through `chat.mcp.listTools()` and `chat.mcp.callTool()`. diff --git a/apps/docs/content/docs/api/chat.mdx b/apps/docs/content/docs/api/chat.mdx index c9f23339..91f27a53 100644 --- a/apps/docs/content/docs/api/chat.mdx +++ b/apps/docs/content/docs/api/chat.mdx @@ -40,6 +40,11 @@ const bot = new Chat(config); type: 'number', default: '500', }, + mcpServers: { + description: 'MCP server configurations for tool discovery and invocation. Requires @modelcontextprotocol/sdk.', + type: 'McpServerConfig[]', + default: 'undefined', + }, }} /> @@ -407,6 +412,45 @@ bot.onAppHomeOpened(async (event) => { }} /> +## mcp + +The `McpManager` instance for interacting with connected MCP servers. Returns a `NoopMcpManager` if no `mcpServers` were configured. + +```typescript +// List all available tools +const tools = await bot.mcp.listTools(); + +// Call a tool +const result = await bot.mcp.callTool("search_issues", { query: "error" }); + +// Call a tool with per-call auth headers +const result = await bot.mcp.callTool( + "search_issues", + { query: "error" }, + { headers: { Authorization: `Bearer ${userToken}` } } +); + +// Refresh tool lists from all servers +await bot.mcp.refresh(); +``` + +', + }, + 'callTool(name, args?, options?)': { + description: 'Invoke a tool by name. Pass options.headers to override auth for this call.', + type: 'Promise', + }, + 'refresh()': { + description: 'Re-fetch tool lists from all connected servers.', + type: 'Promise', + }, + }} +/> + ## Utility methods ### webhooks diff --git a/apps/docs/content/docs/mcp.mdx b/apps/docs/content/docs/mcp.mdx new file mode 100644 index 00000000..a1398f56 --- /dev/null +++ b/apps/docs/content/docs/mcp.mdx @@ -0,0 +1,181 @@ +--- +title: MCP (Model Context Protocol) +description: Discover and invoke tools from remote MCP servers directly from your chat bot handlers. +type: guide +prerequisites: + - /docs/usage +--- + +Chat SDK can connect to remote [MCP](https://modelcontextprotocol.io/) servers to discover and invoke tools. This lets your bot call APIs like Sentry, Linear, Notion, and Figma through a standard protocol. + +## Installation + +The MCP SDK is an optional peer dependency: + +```bash +pnpm add @modelcontextprotocol/sdk +``` + +## Configuration + +Pass `mcpServers` when creating your `Chat` instance: + +```typescript title="lib/bot.ts" lineNumbers +import { Chat } from "chat"; + +const bot = new Chat({ + userName: "mybot", + adapters: { slack }, + state, + mcpServers: [ + { + name: "sentry", + transport: { + type: "http", + url: "https://mcp.sentry.dev/mcp", + headers: { + Authorization: `Bearer ${process.env.SENTRY_AUTH_TOKEN}`, + }, + }, + }, + ], +}); +``` + +MCP servers are connected lazily on the first webhook request. + +## Listing tools + +Discover all tools available across connected servers: + +```typescript title="lib/bot.ts" lineNumbers +const tools = await bot.mcp.listTools(); + +for (const tool of tools) { + console.log(`${tool.serverName}/${tool.name}: ${tool.description}`); +} +``` + +Each tool includes `name`, `description`, `inputSchema` (JSON Schema), and `serverName`. + +## Calling tools + +Invoke a tool by name from any handler: + +```typescript title="lib/bot.ts" lineNumbers +bot.onNewMention(async (thread, message) => { + const result = await bot.mcp.callTool("search_issues", { + query: message.text, + }); + + const text = result.content + .filter((c) => c.type === "text") + .map((c) => c.text) + .join("\n"); + + await thread.post(text); +}); +``` + +## Transport types + +| Type | Config value | Description | +|------|-------------|-------------| +| Streamable HTTP | `"http"` | Default. Modern bidirectional transport over HTTP. | +| Server-Sent Events | `"sse"` | Legacy SSE-based transport. Use if the server doesn't support Streamable HTTP. | + +## Authentication + +### Service-level (static headers) + +The bot uses a shared API key for all requests. Configure headers directly in the transport: + +```typescript +mcpServers: [ + { + name: "sentry", + transport: { + type: "http", + url: "https://mcp.sentry.dev/mcp", + headers: { Authorization: `Bearer ${process.env.SENTRY_AUTH_TOKEN}` }, + }, + }, +]; +``` + +### Per-tenant + +Look up the tenant's token at call time and pass it via the `headers` option. A temporary connection is created with those headers for the duration of the call: + +```typescript title="lib/bot.ts" lineNumbers +bot.onNewMention(async (thread, message) => { + const tenantToken = await lookupTenantToken(thread.id); + + const result = await bot.mcp.callTool( + "search_issues", + { query: message.text }, + { headers: { Authorization: `Bearer ${tenantToken}` } } + ); + + await thread.post(result.content[0].text); +}); +``` + +### Per-user + +Same pattern — resolve the user's OAuth token and pass it per-call: + +```typescript title="lib/bot.ts" lineNumbers +bot.onNewMention(async (thread, message) => { + const userToken = await getUserOAuthToken(message.author.id); + + const result = await bot.mcp.callTool( + "create_issue", + { title: message.text }, + { headers: { Authorization: `Bearer ${userToken}` } } + ); + + await thread.post(`Created: ${result.content[0].text}`); +}); +``` + +## Multiple servers + +Tools from all configured servers are merged. The SDK routes each `callTool` to the correct server automatically: + +```typescript +mcpServers: [ + { + name: "sentry", + transport: { type: "http", url: "https://mcp.sentry.dev/mcp" }, + }, + { + name: "linear", + transport: { + type: "http", + url: "https://mcp.linear.app/mcp", + headers: { Authorization: `Bearer ${process.env.LINEAR_API_KEY}` }, + }, + }, +]; +``` + +## Refreshing tools + +If a server's tool list changes at runtime, re-fetch it: + +```typescript +await bot.mcp.refresh(); +``` + +## Graceful degradation + +If a server fails to connect during initialization, a warning is logged and the remaining servers continue normally. Your bot won't crash because one MCP server is down. + +## Error codes + +| Code | When | +|------|------| +| `MCP_TOOL_NOT_FOUND` | `callTool` is called with a tool name that doesn't exist on any connected server. | +| `MCP_NOT_CONFIGURED` | `callTool` is called but no `mcpServers` were configured. | +| `MCP_SDK_NOT_INSTALLED` | `@modelcontextprotocol/sdk` is not installed. | diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index 71795ff7..267c4108 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -7,6 +7,7 @@ "posting-messages", "---Features---", "...", + "mcp", "error-handling", "---Platform Adapters---", "...adapters", diff --git a/packages/chat/package.json b/packages/chat/package.json index f743d7c7..4226f405 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -41,6 +41,7 @@ "unified": "^11.0.5" }, "devDependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", "@types/mdast": "^4.0.4", "@types/node": "^22.10.2", "tsup": "^8.3.5", @@ -67,5 +68,13 @@ "google-chat", "bot" ], + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.15.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + }, "license": "MIT" } diff --git a/packages/chat/src/chat.ts b/packages/chat/src/chat.ts index ba941c4a..fe064da8 100644 --- a/packages/chat/src/chat.ts +++ b/packages/chat/src/chat.ts @@ -5,6 +5,8 @@ import { setChatSingleton, } from "./chat-singleton"; import { isJSX, toModalElement } from "./jsx-runtime"; +import { McpClientManager, NoopMcpManager } from "./mcp"; +import type { McpManager } from "./mcp-types"; import { Message, type SerializedMessage } from "./message"; import type { ModalElement } from "./modals"; import { type SerializedThread, ThreadImpl } from "./thread"; @@ -181,6 +183,7 @@ export class Chat< private readonly logger: Logger; private readonly _streamingUpdateIntervalMs: number; private readonly _dedupeTtlMs: number; + private readonly mcpManager: McpManager; private readonly mentionHandlers: MentionHandler[] = []; private readonly messagePatterns: MessagePattern[] = []; @@ -208,6 +211,11 @@ export class Chat< */ readonly webhooks: Webhooks; + /** MCP manager for tool discovery and invocation. */ + get mcp(): McpManager { + return this.mcpManager; + } + constructor(config: ChatConfig) { this.userName = config.userName; this._stateAdapter = config.state; @@ -232,6 +240,11 @@ export class Chat< } this.webhooks = webhooks as Webhooks; + // Initialize MCP manager + this.mcpManager = config.mcpServers?.length + ? new McpClientManager(config.mcpServers, this.logger) + : new NoopMcpManager(); + this.logger.debug("Chat instance created", { adapters: Object.keys(config.adapters), }); @@ -289,6 +302,11 @@ export class Chat< ); await Promise.all(initPromises); + // Initialize MCP servers (if configured) + if (this.mcpManager instanceof McpClientManager) { + await this.mcpManager.initialize(); + } + this.initialized = true; this.logger.info("Chat instance initialized", { adapters: Array.from(this.adapters.keys()), @@ -300,6 +318,9 @@ export class Chat< */ async shutdown(): Promise { this.logger.info("Shutting down chat instance..."); + if (this.mcpManager instanceof McpClientManager) { + await this.mcpManager.close(); + } await this._stateAdapter.disconnect(); this.initialized = false; this.initPromise = null; diff --git a/packages/chat/src/index.ts b/packages/chat/src/index.ts index cde5a7c3..a77be1a0 100644 --- a/packages/chat/src/index.ts +++ b/packages/chat/src/index.ts @@ -169,6 +169,17 @@ export { toPlainText, walkAst, } from "./markdown"; +// MCP types +export type { + McpCallToolOptions, + McpContentBlock, + McpHeaders, + McpManager, + McpServerConfig, + McpTool, + McpToolResult, + McpTransportConfig, +} from "./mcp-types"; // Modal types export type { ModalChild, diff --git a/packages/chat/src/mcp-types.ts b/packages/chat/src/mcp-types.ts new file mode 100644 index 00000000..f278e9d6 --- /dev/null +++ b/packages/chat/src/mcp-types.ts @@ -0,0 +1,85 @@ +/** + * Types for MCP (Model Context Protocol) client support. + */ + +/** + * Headers for MCP server authentication. + * Can be a static record or an async function for token rotation. + */ +export type McpHeaders = + | Record + | (() => Record) + | (() => Promise>); + +/** + * Transport configuration for an MCP server. + * Supports SSE and Streamable HTTP transports only (no stdio). + */ +export interface McpTransportConfig { + headers?: McpHeaders; + type: "sse" | "http"; + url: string; +} + +/** + * Configuration for a single MCP server. + */ +export interface McpServerConfig { + name: string; + transport: McpTransportConfig; +} + +/** + * A tool exposed by an MCP server. + */ +export interface McpTool { + description?: string; + inputSchema: Record; + name: string; + serverName: string; +} + +/** + * A content block returned by an MCP tool call. + */ +export type McpContentBlock = + | { type: "text"; text: string } + | { type: "image"; data: string; mimeType: string } + | { + type: "resource"; + resource: { uri: string; mimeType?: string; text?: string }; + }; + +/** + * Result of calling an MCP tool. + */ +export interface McpToolResult { + content: McpContentBlock[]; + isError?: boolean; +} + +/** + * Options for a single `callTool` invocation. + * Pass `headers` to override the server's default auth for this call + * (e.g. per-user or per-tenant tokens). + */ +export interface McpCallToolOptions { + headers?: Record; +} + +/** + * Manager interface for interacting with MCP servers. + */ +export interface McpManager { + /** Call a tool by name with optional arguments and per-call options. */ + callTool( + name: string, + args?: Record, + options?: McpCallToolOptions + ): Promise; + /** List all tools available across connected MCP servers. */ + listTools(): Promise; + + /** Re-fetch tool lists from all connected servers. */ + refresh(): Promise; +} diff --git a/packages/chat/src/mcp.test.ts b/packages/chat/src/mcp.test.ts new file mode 100644 index 00000000..9da2d69b --- /dev/null +++ b/packages/chat/src/mcp.test.ts @@ -0,0 +1,398 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Chat } from "./chat"; +import { ChatError } from "./errors"; +import { McpClientManager, NoopMcpManager } from "./mcp"; +import type { McpServerConfig } from "./mcp-types"; +import { createMockAdapter, createMockState, mockLogger } from "./mock-adapter"; +import type { Adapter, StateAdapter } from "./types"; + +// Mock MCP SDK +const mockConnect = vi.fn().mockResolvedValue(undefined); +const mockListTools = vi.fn().mockResolvedValue({ + tools: [ + { + name: "search_issues", + description: "Search issues", + inputSchema: { type: "object" }, + }, + { + name: "get_event", + description: "Get event details", + inputSchema: { type: "object" }, + }, + ], +}); +const mockCallTool = vi.fn().mockResolvedValue({ + content: [{ type: "text", text: "result" }], + isError: false, +}); +const mockClientClose = vi.fn().mockResolvedValue(undefined); +const mockTransportClose = vi.fn().mockResolvedValue(undefined); + +const MockClient = vi.fn().mockImplementation(() => ({ + connect: mockConnect, + listTools: mockListTools, + callTool: mockCallTool, + close: mockClientClose, +})); + +const MockSSETransport = vi.fn().mockImplementation(() => ({ + close: mockTransportClose, +})); + +const MockHTTPTransport = vi.fn().mockImplementation(() => ({ + close: mockTransportClose, +})); + +vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({ + Client: MockClient, +})); + +vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({ + SSEClientTransport: MockSSETransport, +})); + +vi.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ + StreamableHTTPClientTransport: MockHTTPTransport, +})); + +const serverConfigs: McpServerConfig[] = [ + { + name: "sentry", + transport: { type: "http", url: "https://mcp.sentry.io" }, + }, +]; + +describe("McpClientManager", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockListTools.mockResolvedValue({ + tools: [ + { + name: "search_issues", + description: "Search issues", + inputSchema: { type: "object" }, + }, + { + name: "get_event", + description: "Get event details", + inputSchema: { type: "object" }, + }, + ], + }); + }); + + it("should initialize and list tools with serverName", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + const tools = await manager.listTools(); + expect(tools).toHaveLength(2); + expect(tools[0]).toEqual({ + name: "search_issues", + description: "Search issues", + inputSchema: { type: "object" }, + serverName: "sentry", + }); + expect(tools[1]).toEqual({ + name: "get_event", + description: "Get event details", + inputSchema: { type: "object" }, + serverName: "sentry", + }); + }); + + it("should call a tool and route to the correct server", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + const result = await manager.callTool("search_issues", { query: "slow" }); + expect(mockCallTool).toHaveBeenCalledWith({ + name: "search_issues", + arguments: { query: "slow" }, + }); + expect(result).toEqual({ + content: [{ type: "text", text: "result" }], + isError: false, + }); + }); + + it("should throw MCP_TOOL_NOT_FOUND for unknown tool", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + await expect(manager.callTool("nonexistent")).rejects.toThrow(ChatError); + await expect(manager.callTool("nonexistent")).rejects.toMatchObject({ + code: "MCP_TOOL_NOT_FOUND", + }); + }); + + it("should close all connections", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + await manager.close(); + expect(mockClientClose).toHaveBeenCalled(); + + // After close, listTools should return empty + const tools = await manager.listTools(); + expect(tools).toHaveLength(0); + }); + + it("should log warning for failed server connection and continue", async () => { + mockConnect.mockRejectedValueOnce(new Error("connection refused")); + + const twoServers: McpServerConfig[] = [ + { + name: "failing", + transport: { type: "http", url: "https://fail.example.com" }, + }, + { + name: "working", + transport: { type: "http", url: "https://work.example.com" }, + }, + ]; + + const manager = new McpClientManager(twoServers, mockLogger); + await manager.initialize(); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("failing"), + expect.any(Object) + ); + + // The working server's tools should still be available + const tools = await manager.listTools(); + expect(tools).toHaveLength(2); + }); + + it("should refresh tool lists", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + // Update mock to return different tools + mockListTools.mockResolvedValue({ + tools: [ + { + name: "new_tool", + description: "New tool", + inputSchema: { type: "object" }, + }, + ], + }); + + await manager.refresh(); + const tools = await manager.listTools(); + expect(tools).toHaveLength(1); + expect(tools[0].name).toBe("new_tool"); + }); + + it("should create SSE transport for sse type", async () => { + const sseConfig: McpServerConfig[] = [ + { + name: "sse-server", + transport: { type: "sse", url: "https://sse.example.com" }, + }, + ]; + + const manager = new McpClientManager(sseConfig, mockLogger); + await manager.initialize(); + + expect(MockSSETransport).toHaveBeenCalled(); + expect(MockHTTPTransport).not.toHaveBeenCalled(); + }); + + it("should create HTTP transport for http type", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + expect(MockHTTPTransport).toHaveBeenCalled(); + }); + + it("should use cached client when callTool is called without headers", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + await manager.callTool("search_issues", { query: "slow" }); + + // Should use the client created during initialize, not create a new one + // MockClient is called once during initialize + expect(MockClient).toHaveBeenCalledTimes(1); + expect(mockCallTool).toHaveBeenCalledWith({ + name: "search_issues", + arguments: { query: "slow" }, + }); + }); + + it("should create a temporary client with per-call headers", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + // Reset mocks after initialize + MockClient.mockClear(); + MockHTTPTransport.mockClear(); + mockConnect.mockClear(); + mockCallTool.mockClear(); + mockClientClose.mockClear(); + + await manager.callTool( + "search_issues", + { query: "test" }, + { + headers: { Authorization: "Bearer user-token-123" }, + } + ); + + // Should create a new transport with the override headers + expect(MockHTTPTransport).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ + requestInit: { headers: { Authorization: "Bearer user-token-123" } }, + }) + ); + + // Should create a new client, connect, call, and close + expect(MockClient).toHaveBeenCalledTimes(1); + expect(mockConnect).toHaveBeenCalledTimes(1); + expect(mockCallTool).toHaveBeenCalledWith({ + name: "search_issues", + arguments: { query: "test" }, + }); + expect(mockClientClose).toHaveBeenCalledTimes(1); + }); + + it("should close temporary client even if callTool throws", async () => { + const manager = new McpClientManager(serverConfigs, mockLogger); + await manager.initialize(); + + mockClientClose.mockClear(); + mockCallTool.mockRejectedValueOnce(new Error("tool error")); + + await expect( + manager.callTool( + "search_issues", + {}, + { + headers: { Authorization: "Bearer fail-token" }, + } + ) + ).rejects.toThrow("tool error"); + + // Temporary client should still be closed + expect(mockClientClose).toHaveBeenCalled(); + }); + + it("should pass static headers to transport", async () => { + const configWithHeaders: McpServerConfig[] = [ + { + name: "auth-server", + transport: { + type: "http", + url: "https://auth.example.com", + headers: { Authorization: "Bearer token123" }, + }, + }, + ]; + + const manager = new McpClientManager(configWithHeaders, mockLogger); + await manager.initialize(); + + expect(MockHTTPTransport).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ + requestInit: { headers: { Authorization: "Bearer token123" } }, + }) + ); + }); +}); + +describe("NoopMcpManager", () => { + it("should return empty tools list", async () => { + const manager = new NoopMcpManager(); + const tools = await manager.listTools(); + expect(tools).toEqual([]); + }); + + it("should throw on callTool", async () => { + const manager = new NoopMcpManager(); + await expect(manager.callTool("anything")).rejects.toThrow(ChatError); + await expect(manager.callTool("anything")).rejects.toMatchObject({ + code: "MCP_NOT_CONFIGURED", + }); + }); + + it("should not throw on refresh", async () => { + const manager = new NoopMcpManager(); + await expect(manager.refresh()).resolves.toBeUndefined(); + }); +}); + +describe("Chat MCP wiring", () => { + let mockAdapter: Adapter; + let mockState: StateAdapter; + + beforeEach(() => { + vi.clearAllMocks(); + mockAdapter = createMockAdapter("slack"); + mockState = createMockState(); + }); + + it("should expose mcp getter with NoopMcpManager when no servers configured", () => { + const chat = new Chat({ + userName: "testbot", + adapters: { slack: mockAdapter }, + state: mockState, + logger: mockLogger, + }); + + expect(chat.mcp).toBeInstanceOf(NoopMcpManager); + }); + + it("should expose mcp getter with McpClientManager when servers configured", () => { + const chat = new Chat({ + userName: "testbot", + adapters: { slack: mockAdapter }, + state: mockState, + logger: mockLogger, + mcpServers: serverConfigs, + }); + + expect(chat.mcp).toBeInstanceOf(McpClientManager); + }); + + it("should initialize MCP on first webhook", async () => { + const chat = new Chat({ + userName: "testbot", + adapters: { slack: mockAdapter }, + state: mockState, + logger: mockLogger, + mcpServers: serverConfigs, + }); + + await chat.webhooks.slack( + new Request("http://test.com", { method: "POST" }) + ); + + // MCP client should have been created and connected + expect(MockClient).toHaveBeenCalled(); + expect(mockConnect).toHaveBeenCalled(); + }); + + it("should close MCP on shutdown", async () => { + const chat = new Chat({ + userName: "testbot", + adapters: { slack: mockAdapter }, + state: mockState, + logger: mockLogger, + mcpServers: serverConfigs, + }); + + // Initialize first + await chat.webhooks.slack( + new Request("http://test.com", { method: "POST" }) + ); + + await chat.shutdown(); + expect(mockClientClose).toHaveBeenCalled(); + }); +}); diff --git a/packages/chat/src/mcp.ts b/packages/chat/src/mcp.ts new file mode 100644 index 00000000..dc871382 --- /dev/null +++ b/packages/chat/src/mcp.ts @@ -0,0 +1,336 @@ +/** + * MCP (Model Context Protocol) client manager implementation. + */ + +import { ChatError } from "./errors"; +import type { Logger } from "./logger"; +import type { + McpCallToolOptions, + McpContentBlock, + McpHeaders, + McpManager, + McpServerConfig, + McpTool, + McpToolResult, + McpTransportConfig, +} from "./mcp-types"; + +interface ConnectedServer { + client: McpClient; + name: string; + tools: McpTool[]; + transport: McpTransport; + transportConfig: McpTransportConfig; +} + +// Minimal interfaces for the MCP SDK types we use, to avoid importing at module level. +// These are intentionally loose — the real SDK types are richer but we only use a subset. +interface McpClient { + callTool(params: { + name: string; + arguments?: Record; + }): Promise<{ + content: unknown[]; + isError?: boolean; + }>; + close(): Promise; + connect(transport: McpTransport): Promise; + listTools(): Promise<{ + tools: Array<{ name: string; description?: string; inputSchema?: unknown }>; + }>; +} + +interface McpTransport { + close(): Promise; +} + +interface McpSdk { + Client: new (info: { name: string; version: string }) => McpClient; + SSEClientTransport: new ( + url: URL, + options?: Record + ) => McpTransport; + StreamableHTTPClientTransport: new ( + url: URL, + options?: Record + ) => McpTransport; +} + +async function loadMcpSdk(): Promise { + try { + const [clientMod, sseMod, httpMod] = await Promise.all([ + import("@modelcontextprotocol/sdk/client/index.js"), + import("@modelcontextprotocol/sdk/client/sse.js"), + import("@modelcontextprotocol/sdk/client/streamableHttp.js"), + ]); + return { + Client: clientMod.Client as McpSdk["Client"], + SSEClientTransport: + sseMod.SSEClientTransport as McpSdk["SSEClientTransport"], + StreamableHTTPClientTransport: + httpMod.StreamableHTTPClientTransport as McpSdk["StreamableHTTPClientTransport"], + }; + } catch { + throw new ChatError( + "MCP support requires @modelcontextprotocol/sdk. Install it with: pnpm add @modelcontextprotocol/sdk", + "MCP_SDK_NOT_INSTALLED" + ); + } +} + +function resolveHeaders( + headers: McpHeaders | undefined +): Record | undefined { + if (!headers) { + return undefined; + } + if (typeof headers === "function") { + const result = headers(); + // If it returns a promise, we can't resolve it synchronously — return undefined + // Dynamic headers are handled per-request via custom fetch + if (result instanceof Promise) { + return undefined; + } + return result; + } + return headers; +} + +function createCustomFetch(headers: McpHeaders): typeof globalThis.fetch { + return async (input, init) => { + const resolved = typeof headers === "function" ? await headers() : headers; + const mergedHeaders = { + ...resolved, + ...(init?.headers as Record), + }; + return globalThis.fetch(input, { ...init, headers: mergedHeaders }); + }; +} + +function needsCustomFetch( + headers: McpHeaders | undefined +): headers is McpHeaders { + return typeof headers === "function"; +} + +async function createTransport( + config: McpTransportConfig, + sdk: McpSdk +): Promise { + const url = new URL(config.url); + + if (config.type === "sse") { + const options: Record = {}; + if (needsCustomFetch(config.headers)) { + options.eventSourceInit = { fetch: createCustomFetch(config.headers) }; + } else { + const staticHeaders = resolveHeaders(config.headers); + if (staticHeaders) { + options.requestInit = { headers: staticHeaders }; + } + } + return new sdk.SSEClientTransport(url, options); + } + + // Streamable HTTP + const options: Record = {}; + if (needsCustomFetch(config.headers)) { + // StreamableHTTPClientTransport doesn't support custom fetch natively, + // but we can pass headers via requestInit for static resolution + const resolved = + typeof config.headers === "function" + ? await config.headers() + : config.headers; + options.requestInit = { headers: resolved }; + } else { + const staticHeaders = resolveHeaders(config.headers); + if (staticHeaders) { + options.requestInit = { headers: staticHeaders }; + } + } + return new sdk.StreamableHTTPClientTransport(url, options); +} + +/** + * MCP client manager that connects to multiple MCP servers + * and provides unified tool discovery and invocation. + */ +export class McpClientManager implements McpManager { + private readonly configs: McpServerConfig[]; + private readonly logger: Logger; + private sdk: McpSdk | null = null; + private servers: ConnectedServer[] = []; + + constructor(configs: McpServerConfig[], logger: Logger) { + this.configs = configs; + this.logger = logger; + } + + async initialize(): Promise { + const sdk = await loadMcpSdk(); + this.sdk = sdk; + + const results = await Promise.allSettled( + this.configs.map(async (config) => { + const client = new sdk.Client({ name: "chat-sdk", version: "1.0.0" }); + const transport = await createTransport(config.transport, sdk); + await client.connect(transport); + + const { tools } = await client.listTools(); + const mappedTools: McpTool[] = tools.map((t) => ({ + name: t.name, + description: t.description, + inputSchema: (t.inputSchema as Record) ?? {}, + serverName: config.name, + })); + + this.logger.debug(`MCP server "${config.name}" connected`, { + tools: mappedTools.length, + }); + + return { + name: config.name, + client, + transport, + transportConfig: config.transport, + tools: mappedTools, + } as ConnectedServer; + }) + ); + + for (const [i, result] of results.entries()) { + if (result.status === "fulfilled") { + this.servers.push(result.value); + } else { + this.logger.warn( + `Failed to connect to MCP server "${this.configs[i].name}"`, + { + error: result.reason, + } + ); + } + } + + this.logger.info("MCP initialized", { + connected: this.servers.length, + total: this.configs.length, + }); + } + + async listTools(): Promise { + return this.servers.flatMap((s) => s.tools); + } + + async callTool( + name: string, + args?: Record, + options?: McpCallToolOptions + ): Promise { + const server = this.servers.find((s) => + s.tools.some((t) => t.name === name) + ); + if (!server) { + throw new ChatError( + `MCP tool "${name}" not found on any connected server`, + "MCP_TOOL_NOT_FOUND" + ); + } + + if (options?.headers) { + return this.callToolWithHeaders(server, name, args, options.headers); + } + + const result = await server.client.callTool({ name, arguments: args }); + return { + content: result.content as McpContentBlock[], + isError: result.isError, + }; + } + + private async callToolWithHeaders( + server: ConnectedServer, + name: string, + args: Record | undefined, + headers: Record + ): Promise { + const sdk = this.sdk ?? (await loadMcpSdk()); + const transportConfig: McpTransportConfig = { + ...server.transportConfig, + headers, + }; + const transport = await createTransport(transportConfig, sdk); + const client = new sdk.Client({ name: "chat-sdk", version: "1.0.0" }); + await client.connect(transport); + try { + const result = await client.callTool({ name, arguments: args }); + return { + content: result.content as McpContentBlock[], + isError: result.isError, + }; + } finally { + await client.close().catch(() => {}); + } + } + + async refresh(): Promise { + const results = await Promise.allSettled( + this.servers.map(async (server) => { + const { tools } = await server.client.listTools(); + server.tools = tools.map((t) => ({ + name: t.name, + description: t.description, + inputSchema: (t.inputSchema as Record) ?? {}, + serverName: server.name, + })); + }) + ); + + for (const [i, result] of results.entries()) { + if (result.status === "rejected") { + this.logger.warn( + `Failed to refresh tools from MCP server "${this.servers[i].name}"`, + { + error: result.reason, + } + ); + } + } + } + + async close(): Promise { + await Promise.allSettled( + this.servers.map(async (server) => { + try { + await server.client.close(); + } catch { + // Ignore close errors + } + }) + ); + this.servers = []; + } +} + +/** + * No-op MCP manager used when no MCP servers are configured. + */ +export class NoopMcpManager implements McpManager { + async listTools(): Promise { + return []; + } + + async callTool( + name: string, + _args?: Record, + _options?: McpCallToolOptions + ): Promise { + throw new ChatError( + `MCP is not configured. Cannot call tool "${name}"`, + "MCP_NOT_CONFIGURED" + ); + } + + async refresh(): Promise { + // No-op + } +} diff --git a/packages/chat/src/types.ts b/packages/chat/src/types.ts index 9982e4aa..77532311 100644 --- a/packages/chat/src/types.ts +++ b/packages/chat/src/types.ts @@ -6,6 +6,7 @@ import type { Root } from "mdast"; import type { CardElement } from "./cards"; import type { CardJSXElement } from "./jsx-runtime"; import type { Logger, LogLevel } from "./logger"; +import type { McpServerConfig } from "./mcp-types"; import type { Message } from "./message"; import type { ModalElement } from "./modals"; @@ -46,6 +47,8 @@ export interface ChatConfig< * Pass "silent" to disable all logging. */ logger?: Logger | LogLevel; + /** MCP servers to connect to for tool discovery and invocation */ + mcpServers?: McpServerConfig[]; /** State adapter for subscriptions and locking */ state: StateAdapter; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 566453da..aaed69e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: version: 16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) fumadocs-mdx: specifier: 14.0.4 - version: 14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)) + version: 14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) fumadocs-ui: specifier: 16.2.2 version: 16.2.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18) @@ -435,6 +435,9 @@ importers: specifier: ^11.0.5 version: 11.0.5 devDependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.27.1(zod@4.3.3) '@types/mdast': specifier: ^4.0.4 version: 4.0.4 @@ -1169,6 +1172,12 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -1388,6 +1397,16 @@ packages: stream-browserify: optional: true + '@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 + '@mux/mux-data-google-ima@0.3.4': resolution: {integrity: sha512-j8IOD5kw1qIOkbpipEQRGQ7vXB6+CArrhIAvtvj8YFqy0PHi7JcHk4WR3ZBVy5+5yaRCH+nzHkmJmGsg8g6O5g==} @@ -3066,6 +3085,10 @@ packages: '@workflow/serde@4.1.0-beta.2': resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3095,6 +3118,17 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -3186,6 +3220,10 @@ packages: bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + botbuilder-core@4.23.3: resolution: {integrity: sha512-48iW739I24piBH683b/Unvlu1fSzjB69ViOwZ0PbTkN2yW5cTvHJWlW7bXntO8GSqJfssgPaVthKfyaCW457ig==} @@ -3234,6 +3272,10 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3375,6 +3417,26 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + 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'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -3607,6 +3669,10 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dependency-graph@1.0.0: resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} engines: {node: '>=4'} @@ -3687,12 +3753,19 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -3744,6 +3817,9 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -3777,6 +3853,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -3787,10 +3867,24 @@ packages: 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'} + expect-type@1.3.0: 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@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3807,6 +3901,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3838,6 +3935,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3867,6 +3968,10 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + framer-motion@12.34.0: resolution: {integrity: sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==} peerDependencies: @@ -3881,6 +3986,10 @@ packages: react-dom: optional: true + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} @@ -4114,6 +4223,10 @@ packages: hls.js@1.6.15: resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} + hono@4.12.3: + resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==} + engines: {node: '>=16.9.0'} + html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} @@ -4129,6 +4242,10 @@ packages: htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -4167,6 +4284,9 @@ packages: imsc@1.1.5: resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -4181,6 +4301,14 @@ packages: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} + 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'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -4226,6 +4354,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -4271,6 +4402,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + jotai@2.17.1: resolution: {integrity: sha512-TFNZZDa/0ewCLQyRC/Sq9crtixNj/Xdf/wmj9631xxMuKToVJZDbqcHIYN0OboH+7kh6P6tpIK7uKWClj86PKw==} engines: {node: '>=12.20.0'} @@ -4307,6 +4441,12 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -4603,6 +4743,14 @@ packages: media-tracks@0.3.4: resolution: {integrity: sha512-5SUElzGMYXA7bcyZBL1YzLTxH9Iyw1AeYNJxzByqbestrrtB0F3wfiWUr7aROpwodO4fwnxOt78Xjb3o3ONNQg==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4752,10 +4900,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'} + minimatch@10.2.2: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} @@ -4901,6 +5057,13 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -4971,6 +5134,10 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -5029,6 +5196,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -5082,6 +5253,10 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -5089,6 +5264,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 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==} @@ -5108,6 +5287,14 @@ packages: '@types/react-dom': optional: true + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -5262,6 +5449,10 @@ packages: remend@1.2.0: resolution: {integrity: sha512-NbKrdWweTRuByPYErzQCNpNtsR9M1QQ0hK2UzmnmlSaEqHnkQ5Korlyi8KpdbOJ0rImJfRy4EAY0uDxYnL9Plw==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -5288,6 +5479,10 @@ packages: roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rsa-pem-from-mod-exp@0.8.6: resolution: {integrity: sha512-c5ouQkOvGHF1qomUUDJGFcXsomeSO2gbEs6hVhMAtlkE1CuaZase/WzoaKFG/EZQuNmq6pw/EMCeEnDvOgCJYQ==} @@ -5325,6 +5520,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -5406,6 +5612,10 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -5546,6 +5756,10 @@ packages: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -5636,6 +5850,10 @@ packages: twitch-video-element@0.1.6: resolution: {integrity: sha512-X7l8gy+DEFKJ/EztUwaVnAYwQN9fUJxPkOVJj2sE62sGvGU4DNLyvmOsmVulM+8Plc5dMg6hYIMNRAPaH+39Uw==} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5711,6 +5929,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + url-template@2.0.8: resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} @@ -5763,6 +5985,10 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -5900,6 +6126,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -5938,6 +6167,11 @@ packages: youtube-video-element@1.8.1: resolution: {integrity: sha512-+5UuAGaj+5AnBf39huLVpy/4dLtR0rmJP1TxOHVZ81bac4ZHFpTtQ4Dz2FAn2GPnfXISezvUEaQoAdFW4hH9Xg==} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -6569,6 +6803,10 @@ snapshots: dependencies: graphql: 15.10.1 + '@hono/node-server@1.19.9(hono@4.12.3)': + dependencies: + hono: 4.12.3 + '@iconify/types@2.0.0': {} '@iconify/utils@3.1.0': @@ -6782,6 +7020,28 @@ snapshots: optionalDependencies: '@azure/identity': 4.13.0 + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.3)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.12.3) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + 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: 4.3.3 + zod-to-json-schema: 3.25.1(zod@4.3.3) + transitivePeerDependencies: + - supports-color + '@mux/mux-data-google-ima@0.3.4': dependencies: mux-embed: 5.16.1 @@ -8381,6 +8641,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.3)(lightningcss@1.30.2) + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2))': dependencies: '@vitest/spy': 2.1.9 @@ -8418,6 +8686,11 @@ snapshots: '@workflow/serde@4.1.0-beta.2': {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -8444,6 +8717,17 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.3 + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -8517,6 +8801,20 @@ snapshots: bignumber.js@9.3.1: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + botbuilder-core@4.23.3: dependencies: botbuilder-dialogs-adaptive-runtime-core: 4.23.3-preview @@ -8634,6 +8932,8 @@ snapshots: esbuild: 0.27.2 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -8758,6 +9058,19 @@ snapshots: consola@3.4.2: {} + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -9032,6 +9345,8 @@ snapshots: denque@2.1.0: {} + depd@2.0.0: {} + dependency-graph@1.0.0: {} dequal@2.0.3: {} @@ -9119,10 +9434,14 @@ snapshots: dependencies: safe-buffer: 5.2.1 + ee-first@1.1.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -9223,6 +9542,8 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + escape-html@1.0.3: {} + escape-string-regexp@5.0.0: {} esprima@4.0.1: {} @@ -9264,14 +9585,58 @@ snapshots: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} eventsource-parser@3.0.6: {} + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + 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@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + 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.14.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 + extend@3.0.2: {} extendable-error@0.1.7: {} @@ -9288,6 +9653,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-uri@3.1.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -9314,6 +9681,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + 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-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -9344,6 +9722,8 @@ snapshots: dependencies: fd-package-json: 2.0.0 + forwarded@0.2.0: {} + framer-motion@12.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: motion-dom: 12.34.0 @@ -9353,6 +9733,8 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + fresh@2.0.0: {} + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 @@ -9403,7 +9785,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)): + fumadocs-mdx@14.0.4(fumadocs-core@16.2.2(@types/react@19.2.7)(lucide-react@0.555.0(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -9427,7 +9809,6 @@ snapshots: optionalDependencies: next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 - vite: 5.4.21(@types/node@24.10.13)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color @@ -9736,6 +10117,8 @@ snapshots: hls.js@1.6.15: {} + hono@4.12.3: {} + html-entities@2.6.0: {} html-escaper@2.0.2: {} @@ -9751,6 +10134,14 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 + 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@7.0.2: dependencies: agent-base: 7.1.4 @@ -9787,6 +10178,8 @@ snapshots: dependencies: sax: 1.2.1 + inherits@2.0.4: {} + inline-style-parser@0.2.7: {} internmap@1.0.1: {} @@ -9807,6 +10200,10 @@ snapshots: transitivePeerDependencies: - supports-color + ip-address@10.0.1: {} + + ipaddr.js@1.9.1: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -9838,6 +10235,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-stream@2.0.1: {} is-subdir@1.2.0: @@ -9888,6 +10287,8 @@ snapshots: jiti@2.6.1: {} + jose@6.1.3: {} + jotai@2.17.1(@types/react@19.2.7)(react@19.2.3): optionalDependencies: '@types/react': 19.2.7 @@ -9910,6 +10311,10 @@ snapshots: dependencies: bignumber.js: 9.3.1 + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} jsonc-parser@3.3.1: {} @@ -10299,6 +10704,10 @@ snapshots: media-tracks@0.3.4: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} mermaid@11.12.2: @@ -10627,10 +11036,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 + minimatch@10.2.2: dependencies: brace-expansion: 5.0.2 @@ -10756,6 +11171,14 @@ snapshots: object-inspect@1.13.4: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.4: @@ -10852,6 +11275,8 @@ snapshots: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-data-parser@0.1.0: {} @@ -10890,6 +11315,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -10944,12 +11371,21 @@ snapshots: property-information@7.1.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} qs@6.14.0: dependencies: side-channel: 1.1.0 + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -11017,6 +11453,15 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + unpipe: 1.0.0 + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -11239,6 +11684,8 @@ snapshots: remend@1.2.0: {} + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -11284,6 +11731,16 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rsa-pem-from-mod-exp@0.8.6: {} run-applescript@7.1.0: {} @@ -11310,6 +11767,33 @@ snapshots: semver@7.7.3: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + 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 + + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -11421,6 +11905,8 @@ snapshots: standard-as-callback@2.1.0: {} + statuses@2.0.2: {} + std-env@3.10.0: {} streamdown@2.2.0(react@19.2.3): @@ -11559,6 +12045,8 @@ snapshots: toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + tr46@0.0.3: {} tree-kill@1.2.2: {} @@ -11641,6 +12129,12 @@ snapshots: twitch-video-element@0.1.6: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript@5.9.3: {} ua-parser-js@1.0.41: {} @@ -11720,6 +12214,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + url-template@2.0.8: {} use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3): @@ -11755,6 +12251,8 @@ snapshots: uuid@9.0.1: {} + vary@1.1.2: {} + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -11842,7 +12340,7 @@ snapshots: vitest@2.1.9(@types/node@22.19.3)(lightningcss@1.30.2): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.10.13)(lightningcss@1.30.2)) + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -11964,6 +12462,8 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + ws@7.5.10: {} ws@8.18.3: {} @@ -11980,6 +12480,10 @@ snapshots: youtube-video-element@1.8.1: {} + zod-to-json-schema@3.25.1(zod@4.3.3): + dependencies: + zod: 4.3.3 + zod@3.25.76: {} zod@4.3.3: {}