diff --git a/apps/docs/docs/management/mcp.mdx b/apps/docs/docs/management/mcp.mdx index 724a71c0b..6509dcb3c 100644 --- a/apps/docs/docs/management/mcp.mdx +++ b/apps/docs/docs/management/mcp.mdx @@ -8,7 +8,12 @@ tags: # Model Context Protocol (MCP) -Homarr exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/api/mcp/mcp`, enabling AI assistants like Claude, Cursor, VS Code Copilot, and Claude Code to interact with your Homarr instance using natural language. +Homarr exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/api/mcp`, enabling AI assistants like Claude, Cursor, VS Code Copilot, and Claude Code to interact with your Homarr instance using natural language. + +The endpoint implements the stateless MCP 2026-07-28 protocol and accepts 2025 Streamable HTTP clients through a +stateless compatibility path. It does not create protocol sessions or require sticky sessions. Homarr creates the +server and extracts its tools only after an authenticated request reaches the route. The previous `/api/mcp/mcp` URL +remains available as a compatibility alias, but new clients should always use `/api/mcp`. With MCP, you can ask your AI assistant to manage apps, integrations, users, boards, Docker containers, media servers, DNS settings, and more - all through Homarr's existing tRPC API, now exposed as 50+ AI-callable tools. @@ -46,7 +51,7 @@ Add the following to your client's MCP configuration file: { "mcpServers": { "homarr": { - "url": "https://your-homarr-instance.com/api/mcp/mcp", + "url": "https://your-homarr-instance.com/api/mcp", "headers": { "ApiKey": "" } @@ -77,7 +82,7 @@ Replace `` with your actual API key. Never commit API keys to vers "mcpServers": { "homarr": { "command": "npx", - "args": ["-y", "mcp-remote", "https://your-homarr-instance.com/api/mcp/mcp", "--header", "ApiKey:"] + "args": ["-y", "mcp-remote", "https://your-homarr-instance.com/api/mcp", "--header", "ApiKey:"] } } } @@ -100,6 +105,10 @@ Custom Widget and Workshop authoring are different: their tools, prompts, and re key owner is a Homarr administrator. Non-administrator keys cannot discover or invoke those capabilities. Creating an API key never elevates the owner. +OAuth authorization and token requests bind issued credentials to the canonical `/api/mcp` resource. Dynamic client +registration remains available for compatibility with current MCP clients while the ecosystem transitions toward +pre-registered clients and Client ID Metadata Documents. + ## Testing the Connection Verify your MCP endpoint is working: @@ -107,13 +116,17 @@ Verify your MCP endpoint is working: ```bash curl -s -X POST \ -H "Content-Type: application/json" \ - -H "Accept: application/json, text/event-stream" \ + -H "Accept: application/json" \ -H "ApiKey: " \ - -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' \ - https://your-homarr-instance.com/api/mcp/mcp + -H "Mcp-Method: server/discover" \ + -H "Mcp-Name: homarr" \ + -d '{"jsonrpc":"2.0","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"test","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}},"id":1}' \ + https://your-homarr-instance.com/api/mcp ``` -A successful response includes the server capabilities and available tools. Use `tools/list` to see all available tools: +A successful response includes the supported protocol versions and server capabilities. Use `tools/list` to see all +available tools. MCP v2 clients add the per-request metadata and `Mcp-Method` / `Mcp-Name` headers automatically; the +abbreviated request below uses the stateless 2025 compatibility path for readability: ```bash curl -s -X POST \ @@ -121,9 +134,12 @@ curl -s -X POST \ -H "Accept: application/json, text/event-stream" \ -H "ApiKey: " \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \ - https://your-homarr-instance.com/api/mcp/mcp + https://your-homarr-instance.com/api/mcp ``` +Tool catalogs are returned in deterministic order and may be cached privately by the client for five minutes. The +removed HTTP+SSE `/sse` and `/message` transports return `410 Gone`; use the single Streamable HTTP endpoint instead. + ## Available Tools Homarr exposes 50+ tools organized by namespace. Each tool is either a **query** (GET, read-only) or **mutation** (POST, write action). diff --git a/apps/docs/docs/widgets/custom-api/index.mdx b/apps/docs/docs/widgets/custom-api/index.mdx index 9325a86fe..4217f2369 100644 --- a/apps/docs/docs/widgets/custom-api/index.mdx +++ b/apps/docs/docs/widgets/custom-api/index.mdx @@ -18,7 +18,7 @@ Temporary inputs exist only while the tile is mounted. Homarr encrypts source cr Homarr does not send these credentials to the browser. Homarr does not include them in an export. - + ### Adding the widget diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 9caec3c7a..bca514282 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -67,7 +67,7 @@ "@mantine/hooks": "catalog:", "@mantine/modals": "catalog:", "@mantine/tiptap": "catalog:", - "@modelcontextprotocol/sdk": "catalog:", + "@modelcontextprotocol/server": "catalog:", "@scalar/api-reference-react": "catalog:", "@tabler/icons-react": "catalog:", "@tanstack/react-devtools": "catalog:", @@ -99,7 +99,6 @@ "lucide-react": "catalog:", "mantine-datatable": "catalog:", "mantine-react-table": "catalog:", - "mcp-handler": "catalog:", "mermaid": "catalog:", "next": "catalog:", "postcss-preset-mantine": "catalog:", @@ -115,7 +114,6 @@ "sass": "catalog:", "sql.js": "catalog:", "superjson": "catalog:", - "trpc-to-mcp": "catalog:", "use-deep-compare-effect": "catalog:", "yaml": "catalog:", "zod": "catalog:" diff --git a/apps/nextjs/src/app/.well-known/oauth-authorization-server/route.ts b/apps/nextjs/src/app/.well-known/oauth-authorization-server/route.ts index c400b4020..43b107e2c 100644 --- a/apps/nextjs/src/app/.well-known/oauth-authorization-server/route.ts +++ b/apps/nextjs/src/app/.well-known/oauth-authorization-server/route.ts @@ -22,6 +22,7 @@ export async function GET() { code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"], scopes_supported: ["mcp:tools"], + resource_indicators_supported: true, }, { headers: corsHeaders }, ); diff --git a/apps/nextjs/src/app/.well-known/oauth-protected-resource/route.ts b/apps/nextjs/src/app/.well-known/oauth-protected-resource/route.ts index ca66ee1a3..eb5f22e5a 100644 --- a/apps/nextjs/src/app/.well-known/oauth-protected-resource/route.ts +++ b/apps/nextjs/src/app/.well-known/oauth-protected-resource/route.ts @@ -13,7 +13,7 @@ export async function GET() { return Response.json( { - resource: baseUrl, + resource: `${baseUrl}/api/mcp`, authorization_servers: [baseUrl], }, { headers: corsHeaders }, diff --git a/apps/nextjs/src/app/[locale]/manage/tools/api/components/mcp-instructions.tsx b/apps/nextjs/src/app/[locale]/manage/tools/api/components/mcp-instructions.tsx index f3ac4ea6d..4d2375536 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/api/components/mcp-instructions.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/api/components/mcp-instructions.tsx @@ -63,7 +63,7 @@ export function McpInstructions({ baseUrl, hasApiKeys, toolGroups }: McpInstruct await revalidatePathActionAsync("/manage/tools/api"); }, }); - const mcpUrl = `${baseUrl}/api/mcp/mcp`; + const mcpUrl = `${baseUrl}/api/mcp`; const streamableHttpConfig = JSON.stringify( { @@ -93,7 +93,7 @@ export function McpInstructions({ baseUrl, hasApiKeys, toolGroups }: McpInstruct 2, ); - const testCommand = `curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "ApiKey: " -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' ${mcpUrl}`; + const testCommand = `curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "ApiKey: " -H "Mcp-Method: server/discover" -H "Mcp-Name: homarr" -d '{"jsonrpc":"2.0","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"test","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}},"id":1}' ${mcpUrl}`; return ( diff --git a/apps/nextjs/src/app/api/mcp/[transport]/route.spec.ts b/apps/nextjs/src/app/api/mcp/[transport]/route.spec.ts index 1fdb4e536..2944b02aa 100644 --- a/apps/nextjs/src/app/api/mcp/[transport]/route.spec.ts +++ b/apps/nextjs/src/app/api/mcp/[transport]/route.spec.ts @@ -31,6 +31,8 @@ vi.mock("@homarr/custom-widgets/authoring-prompt", () => ({ buildCustomWidgetMcp vi.mock("@homarr/custom-widgets/authoring-resources", () => ({ getCustomWidgetComponent: mocks.getComponent, getCustomWidgetComponentCatalog: () => ({ components: [{ name: "Text Input" }] }), + getCustomWidgetExample: (name: string) => + name === "status-card" ? { id: "status-card", title: "Status card", template: "Status" } : null, getCustomWidgetSkillContent: () => "# Custom Widget Skill", })); vi.mock("@homarr/custom-widgets/core", () => ({ @@ -220,13 +222,16 @@ describe("non-admin MCP discovery", () => { ]); const prompts = await callMcp("prompts/list"); - expect(prompts.body.result).toEqual({ prompts: [] }); + expect(prompts.body.result).toBeUndefined(); + expect(prompts.body.error).toBeDefined(); const resources = await callMcp("resources/list"); - expect(resources.body.result).toEqual({ resources: [] }); + expect(resources.body.result).toBeUndefined(); + expect(resources.body.error).toBeDefined(); const templates = await callMcp("resources/templates/list"); - expect(templates.body.result).toEqual({ resourceTemplates: [] }); + expect(templates.body.result).toBeUndefined(); + expect(templates.body.error).toBeDefined(); const prompt = await callMcp("prompts/get", { name: "homarr-custom-widget-author" }); expect(prompt.body.result).toBeUndefined(); diff --git a/apps/nextjs/src/app/api/mcp/[transport]/route.ts b/apps/nextjs/src/app/api/mcp/[transport]/route.ts index 048bdcbef..4aa40bffe 100644 --- a/apps/nextjs/src/app/api/mcp/[transport]/route.ts +++ b/apps/nextjs/src/app/api/mcp/[transport]/route.ts @@ -1,443 +1,3 @@ -import { AsyncLocalStorage } from "async_hooks"; -import type { NextRequest } from "next/server"; -import { userAgent } from "next/server"; -import { createMcpHandler } from "mcp-handler"; -import type { McpTool } from "trpc-to-mcp"; -import { - CallToolRequestSchema, - GetPromptRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListResourceTemplatesRequestSchema, - ListToolsRequestSchema, - ReadResourceRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; +import { handleMcpRequest } from "../_handler"; -import { createTRPCContext, mcpRouter } from "@homarr/api/mcp"; -import { API_KEY_HEADER_NAME, getSessionFromApiKeyAsync } from "@homarr/auth/api-key"; -import { extractBaseUrlFromHeaders } from "@homarr/common"; -import { ipAddressFromHeaders } from "@homarr/common/server"; -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { buildCustomWidgetMcpPrompt } from "@homarr/custom-widgets/authoring-prompt"; -import { - getCustomWidgetComponent, - getCustomWidgetComponentCatalog, - getCustomWidgetSkillContent, -} from "@homarr/custom-widgets/authoring-resources"; -import { customJsxExamples, getCustomWidgetJsonSchema } from "@homarr/custom-widgets/core"; -import { db } from "@homarr/db"; - -import { getPackageVersion } from "~/versions/package-reader"; -import { extractMcpTools } from "../_extract-tools"; - -const logger = createLogger({ module: "mcpRoute" }); - -const AUTH_RATE_WINDOW_MS = 60_000; -const AUTH_MAX_FAILURES = 10; - -interface RateLimitStore { - authFailures: Map; - cleanupTimer: ReturnType | null; -} - -declare global { - var mcpRateLimit: RateLimitStore | undefined; -} - -const rateLimitStore = - globalThis.mcpRateLimit ?? - (globalThis.mcpRateLimit = { - authFailures: new Map(), - cleanupTimer: null, - }); -const authFailures = rateLimitStore.authFailures; - -if (!rateLimitStore.cleanupTimer) { - rateLimitStore.cleanupTimer = setInterval(() => { - const now = Date.now(); - for (const [key, entry] of authFailures) { - if (entry.resetAt < now) authFailures.delete(key); - } - }, AUTH_RATE_WINDOW_MS); - rateLimitStore.cleanupTimer.unref(); -} - -function checkAuthRateLimit(ip: string | null): boolean { - const key = ip ?? "unknown"; - const now = Date.now(); - const entry = authFailures.get(key); - if (!entry || entry.resetAt < now) return true; - return entry.count < AUTH_MAX_FAILURES; -} - -function recordAuthFailure(ip: string | null) { - const key = ip ?? "unknown"; - const now = Date.now(); - const entry = authFailures.get(key); - if (!entry || entry.resetAt < now) { - authFailures.set(key, { count: 1, resetAt: now + AUTH_RATE_WINDOW_MS }); - return; - } - entry.count++; -} - -function clearAuthFailures(ip: string | null) { - authFailures.delete(ip ?? "unknown"); -} - -interface McpCallerContext { - caller: ReturnType; - isAdmin: boolean; -} - -const callerStorage = new AsyncLocalStorage(); - -const emptyObjectSchema = { - type: "object" as const, - properties: {}, - required: [] as string[], -}; - -let toolsCache: McpTool[] | null = null; - -function normalizeSchema(schema: McpTool["inputSchema"]) { - if (!schema) return emptyObjectSchema; - const properties = (schema.properties ?? {}) as Record>; - const required = ((schema.required as string[]) ?? []).filter((key) => !("default" in (properties[key] ?? {}))); - return { type: schema.type ?? ("object" as const), properties, required }; -} - -function getTools() { - if (!toolsCache) { - toolsCache = extractMcpTools().map((tool) => ({ - ...tool, - inputSchema: normalizeSchema(tool.inputSchema), - })); - logger.info(`Extracted ${toolsCache.length} MCP tools`); - } - return toolsCache; -} - -function getVisibleTools() { - const tools = getTools(); - if (!isCustomWidgetMcpEnabled()) { - return tools.filter((tool) => !tool.name.startsWith("customWidget_")); - } - return tools; -} - -function requireCustomWidgetMcpAdmin() { - if (!isCustomWidgetMcpEnabled()) throw new Error("Resource not found"); -} - -const isCustomWidgetMcpEnabled = () => Boolean(callerStorage.getStore()?.isAdmin); - -const SAFE_ERROR_MESSAGES: Record = { - UNAUTHORIZED: "You do not have permission to perform this action", - FORBIDDEN: "Access denied", - NOT_FOUND: "The requested resource was not found", - BAD_REQUEST: "Invalid request parameters", - INTERNAL_SERVER_ERROR: "An internal error occurred", - TIMEOUT: "The operation timed out", - TOO_MANY_REQUESTS: "Rate limit exceeded, try again later", - CONFLICT: "A conflict occurred with the current state", - PRECONDITION_FAILED: "A precondition for this operation was not met", -}; - -function sanitizeErrorMessage(error: unknown): string { - if (error instanceof Error && "code" in error) { - const trpcCode = (error as Error & { code: string }).code; - const safeMessage = SAFE_ERROR_MESSAGES[trpcCode]; - if (safeMessage) return safeMessage; - } - if (error instanceof Error && error.message.length < 200 && !error.message.includes("/")) { - return error.message; - } - return "Tool execution failed"; -} - -const SERVER_INSTRUCTIONS = `You are connected to Homarr, a self-hosted homelab dashboard. - -## Key Concepts - -**Integrations** are connections to external self-hosted services (Sonarr, Radarr, Plex, Overseerr, Pi-hole, Home Assistant, etc.). Each integration has an \`id\` (integrationId) and a \`kind\` (e.g. "sonarr", "radarr", "overseerr"). Use \`integration_all\` to list them. Many tools require an \`integrationId\` — always fetch it from \`integration_all\` or \`integration_search\` first. - -**Boards** are customizable dashboards containing widgets that display data from integrations. Users can have multiple boards. - -**Apps** are links/bookmarks to self-hosted services displayed on boards. They are separate from integrations — an app is a visual shortcut, an integration is a data connection. - -**Permissions**: Each integration result includes a \`permissions\` object: -- \`hasUseAccess: true\` means you can read data from this integration -- \`hasInteractAccess: true\` means you can perform actions (start/stop, request media, etc.) -- \`hasFullAccess: true\` means you can modify/delete the integration config -If \`hasUseAccess\` is false, the API key owner lacks permission for that integration — this is normal, not an error. Simply skip integrations where you lack the required permission level. - -## Common Workflows - -**"What's coming this week?"** → Use \`calendar_findAllEvents\` to get upcoming media releases from Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books). - -**"Search and request a movie/show"** → Use \`integration_searchMediaRequests\` to search across Overseerr/Jellyseerr, then \`integration_requestMedia\` to submit a request. Use \`integration_getMediaRequestOptions\` first for TV shows to pick seasons. - -**"What's streaming right now?"** → Use \`mediaServer_getCurrentStreams\` to see active Plex/Jellyfin/Emby streams. - -**"Check server health"** → Use \`healthMonitoring_getSystemHealthStatus\` for NAS/server metrics or \`healthMonitoring_getClusterHealthStatus\` for Proxmox clusters. - -**"Check Beszel systems"** → Call \`integration_all\` to get Beszel integrationIds, then use \`beszel_getSystems\` to list all monitored systems with CPU/memory/disk/network status. Use \`beszel_getAlerts\` for active alerts and history. Use \`beszel_getSystemStats\` for historical metrics of a specific system (requires systemId from \`beszel_getSystems\`). - -**"Manage Docker containers"** → Use \`docker_getContainers\` to list containers with status/CPU/memory, then \`docker_startAll\`/\`docker_stopAll\`/\`docker_restartAll\` with container IDs. - -**"Control smart home"** → Use \`smartHome_entityState\` to check a Home Assistant entity, \`smartHome_switchEntity\` to toggle it, or \`smartHome_executeAutomation\` to trigger an automation. - -**"Block/unblock ads"** → Use \`dnsHole_summary\` for Pi-hole/AdGuard stats, \`dnsHole_disable\` to temporarily disable blocking, \`dnsHole_enable\` to re-enable. - -**"Check downloads"** → Use \`downloads_getJobsAndStatuses\` for queue status, \`downloads_pause\`/\`downloads_resume\` to control queues. - -**"Pending media requests"** → Use \`mediaRequests_getLatestRequests\` to see pending/approved/declined requests, \`mediaRequests_answerRequest\` to approve or decline. - -## Tips -- Always call \`integration_all\` first to discover available integrations and their IDs before using integration-dependent tools. -- The \`integrationId\` parameter in widget tools accepts the ID from \`integration_all\`, not the integration name or kind. -- If a tool returns empty results, the user may not have the required integration configured. -- Docker and Kubernetes tools work only if the Homarr instance has access to the Docker socket or Kubernetes cluster.`; - -const mcpHandler = createMcpHandler( - (server) => { - server.server.setRequestHandler(ListPromptsRequestSchema, () => ({ - prompts: isCustomWidgetMcpEnabled() - ? [ - { - name: "homarr-custom-widget-author", - description: "Author and iterate on one Homarr Custom JSX v2 widget.", - arguments: [ - { name: "request", description: "The widget the user wants.", required: false }, - { name: "documentationUrl", description: "External API documentation URL.", required: false }, - ], - }, - ] - : [], - })); - - server.server.setRequestHandler(GetPromptRequestSchema, (request) => { - requireCustomWidgetMcpAdmin(); - if (request.params.name !== "homarr-custom-widget-author") throw new Error("Prompt not found"); - return { - description: "Current Homarr Custom Widget authoring workflow.", - messages: [ - { - role: "user" as const, - content: { - type: "text" as const, - text: buildCustomWidgetMcpPrompt( - request.params.arguments?.request, - request.params.arguments?.documentationUrl, - ), - }, - }, - ], - }; - }); - - server.server.setRequestHandler(ListResourcesRequestSchema, () => ({ - resources: isCustomWidgetMcpEnabled() - ? [ - { - uri: "homarr://custom-widgets/schema", - name: "Custom Widget schema", - mimeType: "application/schema+json", - }, - { - uri: "homarr://custom-widgets/components", - name: "Custom Widget components", - mimeType: "application/json", - }, - { uri: "homarr://custom-widgets/skill", name: "Custom Widget skill", mimeType: "text/markdown" }, - ...customJsxExamples.map((example) => ({ - uri: `homarr://custom-widgets/examples/${example.id}`, - name: `Custom Widget example: ${example.title}`, - mimeType: "application/json", - })), - ] - : [], - })); - - server.server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ - resourceTemplates: isCustomWidgetMcpEnabled() - ? [ - { - uriTemplate: "homarr://custom-widgets/components/{name}", - name: "Custom Widget component", - description: "Metadata for one installed Custom Widget component.", - mimeType: "application/json", - }, - { - uriTemplate: "homarr://custom-widgets/examples/{name}", - name: "Custom Widget example", - description: "One canonical Custom Widget example.", - mimeType: "application/json", - }, - ] - : [], - })); - - server.server.setRequestHandler(ReadResourceRequestSchema, (request) => { - requireCustomWidgetMcpAdmin(); - const { uri } = request.params; - let text: string; - let mimeType = "application/json"; - if (uri === "homarr://custom-widgets/schema") { - text = JSON.stringify(getCustomWidgetJsonSchema()); - mimeType = "application/schema+json"; - } else if (uri === "homarr://custom-widgets/components") text = JSON.stringify(getCustomWidgetComponentCatalog()); - else if (uri === "homarr://custom-widgets/skill") { - text = getCustomWidgetSkillContent(); - mimeType = "text/markdown"; - } else if (uri.startsWith("homarr://custom-widgets/components/")) { - const name = decodeURIComponent(uri.slice("homarr://custom-widgets/components/".length)); - const component = getCustomWidgetComponent(name); - if (!component) throw new Error("Component resource not found"); - text = JSON.stringify(component); - } else if (uri.startsWith("homarr://custom-widgets/examples/")) { - const name = decodeURIComponent(uri.slice("homarr://custom-widgets/examples/".length)); - const example = customJsxExamples.find((candidate) => candidate.id === name); - if (!example) throw new Error("Example resource not found"); - text = JSON.stringify(example); - } else throw new Error("Resource not found"); - return { contents: [{ uri, mimeType, text }] }; - }); - - server.server.setRequestHandler(ListToolsRequestSchema, () => ({ - tools: getVisibleTools(), - })); - - server.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - const tools = getVisibleTools(); - const tool = tools.find((t) => t.name === name); - - if (!tool) { - return { - content: [{ type: "text" as const, text: `Tool "${name}" not found` }], - isError: true, - }; - } - - const callerContext = callerStorage.getStore(); - if (!callerContext) { - return { - content: [{ type: "text" as const, text: "Authentication context not available" }], - isError: true, - }; - } - - try { - const procedure = tool.pathInRouter.reduce((acc, part) => acc?.[part], callerContext.caller); - if (typeof procedure !== "function") { - return { - content: [{ type: "text" as const, text: `Tool "${name}" is not callable` }], - isError: true, - }; - } - const input = args && Object.keys(args).length > 0 ? args : undefined; - const result = await procedure(input); - return { - content: [{ type: "text" as const, text: JSON.stringify(result) }], - }; - } catch (error) { - const message = sanitizeErrorMessage(error); - logger.warn("MCP tool execution failed", { - tool: name, - errorName: error instanceof Error ? error.name : "UnknownError", - errorCode: - error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined, - }); - return { - content: [{ type: "text" as const, text: JSON.stringify({ error: message }) }], - isError: true, - }; - } - }); - }, - { - capabilities: { tools: {}, prompts: {}, resources: {} }, - instructions: SERVER_INSTRUCTIONS, - serverInfo: { - name: "homarr", - version: getPackageVersion(), - }, - }, - { - basePath: "/api/mcp", - maxDuration: 60, - }, -); - -function extractApiKeyValue(req: NextRequest): string | null { - const apiKeyHeader = req.headers.get(API_KEY_HEADER_NAME); - if (apiKeyHeader) return apiKeyHeader; - - const authHeader = req.headers.get("Authorization"); - if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7); - - return null; -} - -function jsonErrorResponse(status: number, body: Record, extraHeaders: Record = {}) { - return new Response(JSON.stringify(body), { - status, - headers: { "Content-Type": "application/json", ...extraHeaders }, - }); -} - -const handler = async (req: NextRequest) => { - const apiKeyValue = extractApiKeyValue(req); - const ipAddress = ipAddressFromHeaders(req.headers); - const { ua } = userAgent(req); - - if (!checkAuthRateLimit(ipAddress)) { - return jsonErrorResponse( - 429, - { error: "rate_limited", hint: "Too many failed authentication attempts. Try again later." }, - { "Retry-After": "60" }, - ); - } - - if (!apiKeyValue) { - recordAuthFailure(ipAddress); - const baseUrl = extractBaseUrlFromHeaders(req.headers); - return jsonErrorResponse( - 401, - { - error: "unauthorized", - hint: "Authenticate with an ApiKey header or via OAuth at /.well-known/oauth-authorization-server", - }, - { "WWW-Authenticate": `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"` }, - ); - } - - if (!apiKeyValue.includes(".")) { - recordAuthFailure(ipAddress); - return jsonErrorResponse(401, { error: "invalid_token", hint: "The token must be in the format '.'." }); - } - - const session = await getSessionFromApiKeyAsync(db, apiKeyValue, ipAddress, ua); - - if (!session) { - recordAuthFailure(ipAddress); - return jsonErrorResponse(401, { - error: "invalid_token", - hint: "The API key was not found or the token is incorrect.", - }); - } - - clearAuthFailures(ipAddress); - logger.info("MCP request authenticated", { userId: session.user.id }); - - const ctx = createTRPCContext({ session, headers: req.headers }); - const caller = mcpRouter.createCaller(ctx); - - return callerStorage.run({ caller, isAdmin: session.user.permissions.includes("admin") }, () => - mcpHandler(req as unknown as Request), - ); -}; - -export { handler as GET, handler as POST, handler as DELETE }; +export { handleMcpRequest as DELETE, handleMcpRequest as GET, handleMcpRequest as POST }; diff --git a/apps/nextjs/src/app/api/mcp/_extract-tools.ts b/apps/nextjs/src/app/api/mcp/_extract-tools.ts index 431ab9116..5b90d2963 100644 --- a/apps/nextjs/src/app/api/mcp/_extract-tools.ts +++ b/apps/nextjs/src/app/api/mcp/_extract-tools.ts @@ -1,23 +1,11 @@ -import { extractToolsFromProcedures } from "trpc-to-mcp"; +import { extractMcpToolsFromProcedures, mcpRouter } from "@homarr/api/mcp"; -import { mcpRouter } from "@homarr/api/mcp"; - -let cache: ReturnType | null = null; +let cache: ReturnType | null = null; export function extractMcpTools() { if (cache) return cache; - const originalWarn = console.warn; - console.warn = (...args: unknown[]) => { - if (typeof args[0] === "string" && args[0].includes("[TRPC-TO-MCP]")) return; - originalWarn.apply(console, args); - }; - - try { - cache = extractToolsFromProcedures(mcpRouter); - } finally { - console.warn = originalWarn; - } + cache = extractMcpToolsFromProcedures(mcpRouter); return cache; } diff --git a/apps/nextjs/src/app/api/mcp/_handler.ts b/apps/nextjs/src/app/api/mcp/_handler.ts new file mode 100644 index 000000000..ffb3bd73d --- /dev/null +++ b/apps/nextjs/src/app/api/mcp/_handler.ts @@ -0,0 +1,360 @@ +import type { NextRequest } from "next/server"; +import { userAgent } from "next/server"; +import { ResourceTemplate, type McpServer } from "@modelcontextprotocol/server"; +import { z } from "zod/v4"; + +import type { McpTool } from "@homarr/api/mcp"; +import { createTRPCContext, mcpRouter } from "@homarr/api/mcp"; +import { API_KEY_HEADER_NAME, getSessionFromApiKeyAsync } from "@homarr/auth/api-key"; +import { extractBaseUrlFromHeaders } from "@homarr/common"; +import { ipAddressFromHeaders } from "@homarr/common/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { buildCustomWidgetMcpPrompt } from "@homarr/custom-widgets/authoring-prompt"; +import { + getCustomWidgetComponent, + getCustomWidgetComponentCatalog, + getCustomWidgetExample, + getCustomWidgetSkillContent, +} from "@homarr/custom-widgets/authoring-resources"; +import { customJsxExamples, getCustomWidgetJsonSchema } from "@homarr/custom-widgets/core"; +import { db } from "@homarr/db"; + +import { getPackageVersion } from "~/versions/package-reader"; +import { extractMcpTools } from "./_extract-tools"; +import { createMcpProtocolHandler } from "./_protocol"; + +const logger = createLogger({ module: "mcpRoute" }); + +const AUTH_RATE_WINDOW_MS = 60_000; +const AUTH_MAX_FAILURES = 10; + +interface RateLimitStore { + authFailures: Map; + cleanupTimer: ReturnType | null; +} + +declare global { + var mcpRateLimit: RateLimitStore | undefined; +} + +const rateLimitStore = + globalThis.mcpRateLimit ?? + (globalThis.mcpRateLimit = { + authFailures: new Map(), + cleanupTimer: null, + }); +const authFailures = rateLimitStore.authFailures; + +if (!rateLimitStore.cleanupTimer) { + rateLimitStore.cleanupTimer = setInterval(() => { + const now = Date.now(); + for (const [key, entry] of authFailures) { + if (entry.resetAt < now) authFailures.delete(key); + } + }, AUTH_RATE_WINDOW_MS); + rateLimitStore.cleanupTimer.unref(); +} + +function checkAuthRateLimit(ip: string | null): boolean { + const key = ip ?? "unknown"; + const now = Date.now(); + const entry = authFailures.get(key); + if (!entry || entry.resetAt < now) return true; + return entry.count < AUTH_MAX_FAILURES; +} + +function recordAuthFailure(ip: string | null) { + const key = ip ?? "unknown"; + const now = Date.now(); + const entry = authFailures.get(key); + if (!entry || entry.resetAt < now) { + authFailures.set(key, { count: 1, resetAt: now + AUTH_RATE_WINDOW_MS }); + return; + } + entry.count++; +} + +function clearAuthFailures(ip: string | null) { + authFailures.delete(ip ?? "unknown"); +} + +let toolsCache: McpTool[] | null = null; + +function getTools() { + if (!toolsCache) { + toolsCache = extractMcpTools(); + logger.info(`Extracted ${toolsCache.length} MCP tools`); + } + return toolsCache; +} + +const getVisibleTools = (isAdmin: boolean) => + isAdmin ? getTools() : getTools().filter((tool) => !tool.name.startsWith("customWidget_")); + +function registerCustomWidgetAuthoring(server: McpServer) { + server.registerPrompt( + "homarr-custom-widget-author", + { + description: "Author and iterate on one Homarr Custom JSX v2 widget.", + argsSchema: z.object({ + request: z.string().optional().describe("The widget the user wants."), + documentationUrl: z.string().optional().describe("External API documentation URL."), + }), + }, + ({ request, documentationUrl }) => ({ + description: "Current Homarr Custom Widget authoring workflow.", + messages: [ + { + role: "user" as const, + content: { + type: "text" as const, + text: buildCustomWidgetMcpPrompt(request, documentationUrl), + }, + }, + ], + }), + ); + + server.registerResource( + "Custom Widget schema", + "homarr://custom-widgets/schema", + { mimeType: "application/schema+json" }, + (uri) => ({ + contents: [ + { + uri: uri.href, + mimeType: "application/schema+json", + text: JSON.stringify(getCustomWidgetJsonSchema()), + }, + ], + }), + ); + server.registerResource( + "Custom Widget components", + "homarr://custom-widgets/components", + { mimeType: "application/json" }, + (uri) => ({ + contents: [ + { uri: uri.href, mimeType: "application/json", text: JSON.stringify(getCustomWidgetComponentCatalog()) }, + ], + }), + ); + server.registerResource( + "Custom Widget skill", + "homarr://custom-widgets/skill", + { mimeType: "text/markdown" }, + (uri) => ({ + contents: [{ uri: uri.href, mimeType: "text/markdown", text: getCustomWidgetSkillContent() }], + }), + ); + + for (const example of customJsxExamples) { + server.registerResource( + `Custom Widget example: ${example.title}`, + `homarr://custom-widgets/examples/${example.id}`, + { mimeType: "application/json" }, + (uri) => ({ + contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(example) }], + }), + ); + } + + server.registerResource( + "Custom Widget component", + new ResourceTemplate("homarr://custom-widgets/components/{name}", { list: undefined }), + { + description: "Metadata for one installed Custom Widget component.", + mimeType: "application/json", + }, + (uri, { name }) => { + const component = getCustomWidgetComponent(decodeURIComponent(String(name))); + if (!component) throw new Error("Component resource not found"); + return { + contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(component) }], + }; + }, + ); + server.registerResource( + "Custom Widget example", + new ResourceTemplate("homarr://custom-widgets/examples/{name}", { list: undefined }), + { + description: "One canonical Custom Widget example.", + mimeType: "application/json", + }, + (uri, { name }) => { + const example = getCustomWidgetExample(decodeURIComponent(String(name))); + if (!example) throw new Error("Example resource not found"); + return { + contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(example) }], + }; + }, + ); +} + +const SAFE_ERROR_MESSAGES: Record = { + UNAUTHORIZED: "You do not have permission to perform this action", + FORBIDDEN: "Access denied", + NOT_FOUND: "The requested resource was not found", + BAD_REQUEST: "Invalid request parameters", + INTERNAL_SERVER_ERROR: "An internal error occurred", + TIMEOUT: "The operation timed out", + TOO_MANY_REQUESTS: "Rate limit exceeded, try again later", + CONFLICT: "A conflict occurred with the current state", + PRECONDITION_FAILED: "A precondition for this operation was not met", +}; + +function sanitizeErrorMessage(error: unknown): string { + if (error instanceof Error && "code" in error) { + const trpcCode = (error as Error & { code: string }).code; + const safeMessage = SAFE_ERROR_MESSAGES[trpcCode]; + if (safeMessage) return safeMessage; + } + if (error instanceof Error && error.message.length < 200 && !error.message.includes("/")) { + return error.message; + } + return "Tool execution failed"; +} + +const SERVER_INSTRUCTIONS = `You are connected to Homarr, a self-hosted homelab dashboard. + +## Key Concepts + +**Integrations** are connections to external self-hosted services (Sonarr, Radarr, Plex, Overseerr, Pi-hole, Home Assistant, etc.). Each integration has an \`id\` (integrationId) and a \`kind\` (e.g. "sonarr", "radarr", "overseerr"). Use \`integration_all\` to list them. Many tools require an \`integrationId\` — always fetch it from \`integration_all\` or \`integration_search\` first. + +**Boards** are customizable dashboards containing widgets that display data from integrations. Users can have multiple boards. + +**Apps** are links/bookmarks to self-hosted services displayed on boards. They are separate from integrations — an app is a visual shortcut, an integration is a data connection. + +**Permissions**: Each integration result includes a \`permissions\` object: +- \`hasUseAccess: true\` means you can read data from this integration +- \`hasInteractAccess: true\` means you can perform actions (start/stop, request media, etc.) +- \`hasFullAccess: true\` means you can modify/delete the integration config +If \`hasUseAccess\` is false, the API key owner lacks permission for that integration — this is normal, not an error. Simply skip integrations where you lack the required permission level. + +## Common Workflows + +**"What's coming this week?"** → Use \`calendar_findAllEvents\` to get upcoming media releases from Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books). + +**"Search and request a movie/show"** → Use \`integration_searchMediaRequests\` to search across Overseerr/Jellyseerr, then \`integration_requestMedia\` to submit a request. Use \`integration_getMediaRequestOptions\` first for TV shows to pick seasons. + +**"What's streaming right now?"** → Use \`mediaServer_getCurrentStreams\` to see active Plex/Jellyfin/Emby streams. + +**"Check server health"** → Use \`healthMonitoring_getSystemHealthStatus\` for NAS/server metrics or \`healthMonitoring_getClusterHealthStatus\` for Proxmox clusters. + +**"Check Beszel systems"** → Call \`integration_all\` to get Beszel integrationIds, then use \`beszel_getSystems\` to list all monitored systems with CPU/memory/disk/network status. Use \`beszel_getAlerts\` for active alerts and history. Use \`beszel_getSystemStats\` for historical metrics of a specific system (requires systemId from \`beszel_getSystems\`). + +**"Manage Docker containers"** → Use \`docker_getContainers\` to list containers with status/CPU/memory, then \`docker_startAll\`/\`docker_stopAll\`/\`docker_restartAll\` with container IDs. + +**"Control smart home"** → Use \`smartHome_entityState\` to check a Home Assistant entity, \`smartHome_switchEntity\` to toggle it, or \`smartHome_executeAutomation\` to trigger an automation. + +**"Block/unblock ads"** → Use \`dnsHole_summary\` for Pi-hole/AdGuard stats, \`dnsHole_disable\` to temporarily disable blocking, \`dnsHole_enable\` to re-enable. + +**"Check downloads"** → Use \`downloads_getJobsAndStatuses\` for queue status, \`downloads_pause\`/\`downloads_resume\` to control queues. + +**"Pending media requests"** → Use \`mediaRequests_getLatestRequests\` to see pending/approved/declined requests, \`mediaRequests_answerRequest\` to approve or decline. + +## Tips +- Always call \`integration_all\` first to discover available integrations and their IDs before using integration-dependent tools. +- The \`integrationId\` parameter in widget tools accepts the ID from \`integration_all\`, not the integration name or kind. +- If a tool returns empty results, the user may not have the required integration configured. +- Docker and Kubernetes tools work only if the Homarr instance has access to the Docker socket or Kubernetes cluster.`; + +function extractApiKeyValue(req: NextRequest): string | null { + const apiKeyHeader = req.headers.get(API_KEY_HEADER_NAME); + if (apiKeyHeader) return apiKeyHeader; + + const authHeader = req.headers.get("Authorization"); + if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7); + + return null; +} + +function jsonErrorResponse(status: number, body: Record, extraHeaders: Record = {}) { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json", ...extraHeaders }, + }); +} + +export const handleMcpRequest = async (req: NextRequest) => { + const pathname = new URL(req.url).pathname; + if (pathname.endsWith("/sse") || pathname.endsWith("/message")) { + return jsonErrorResponse(410, { + error: "transport_removed", + hint: "Legacy HTTP+SSE was removed. Connect to the stateless Streamable HTTP endpoint at /api/mcp.", + }); + } + + const apiKeyValue = extractApiKeyValue(req); + const ipAddress = ipAddressFromHeaders(req.headers); + const { ua } = userAgent(req); + + if (!checkAuthRateLimit(ipAddress)) { + return jsonErrorResponse( + 429, + { error: "rate_limited", hint: "Too many failed authentication attempts. Try again later." }, + { "Retry-After": "60" }, + ); + } + + if (!apiKeyValue) { + recordAuthFailure(ipAddress); + const baseUrl = extractBaseUrlFromHeaders(req.headers); + return jsonErrorResponse( + 401, + { + error: "unauthorized", + hint: "Authenticate with an ApiKey header or via OAuth at /.well-known/oauth-authorization-server", + }, + { "WWW-Authenticate": `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"` }, + ); + } + + if (!apiKeyValue.includes(".")) { + recordAuthFailure(ipAddress); + return jsonErrorResponse(401, { error: "invalid_token", hint: "The token must be in the format '.'." }); + } + + const session = await getSessionFromApiKeyAsync(db, apiKeyValue, ipAddress, ua); + + if (!session) { + recordAuthFailure(ipAddress); + return jsonErrorResponse(401, { + error: "invalid_token", + hint: "The API key was not found or the token is incorrect.", + }); + } + + clearAuthFailures(ipAddress); + logger.info("MCP request authenticated", { userId: session.user.id }); + + const ctx = createTRPCContext({ session, headers: req.headers }); + const caller = mcpRouter.createCaller(ctx); + const isAdmin = session.user.permissions.includes("admin"); + const protocolHandler = createMcpProtocolHandler({ + caller, + tools: getVisibleTools(isAdmin), + version: getPackageVersion(), + instructions: SERVER_INSTRUCTIONS, + configureServer: isAdmin ? registerCustomWidgetAuthoring : undefined, + formatToolError: sanitizeErrorMessage, + onToolError: (tool, error) => { + logger.warn("MCP tool execution failed", { + tool, + errorName: error instanceof Error ? error.name : "UnknownError", + errorCode: error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined, + }); + }, + onProtocolError: (error) => { + logger.warn("MCP protocol request failed", { error: error.message }); + }, + }); + + return protocolHandler.fetch(req, { + authInfo: { + token: apiKeyValue, + clientId: session.user.id, + scopes: ["mcp:tools"], + }, + }); +}; diff --git a/apps/nextjs/src/app/api/mcp/_protocol.spec.ts b/apps/nextjs/src/app/api/mcp/_protocol.spec.ts new file mode 100644 index 000000000..3259afb0f --- /dev/null +++ b/apps/nextjs/src/app/api/mcp/_protocol.spec.ts @@ -0,0 +1,140 @@ +// @vitest-environment node + +import { describe, expect, test, vi } from "vitest"; +import { z } from "zod/v4"; + +import type { McpTool } from "@homarr/api/mcp"; + +import { createMcpProtocolHandler } from "./_protocol"; + +const modernMetadata = { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { name: "homarr-test", version: "1.0.0" }, + "io.modelcontextprotocol/clientCapabilities": {}, +}; + +const createRequest = (method: string, params: Record = {}, name = "homarr") => + new Request("http://homarr.test/api/mcp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + "Mcp-Method": method, + "Mcp-Name": name, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method, + params: { ...params, _meta: modernMetadata }, + }), + }); + +const parseResponse = async (response: Response) => { + expect(response.status).toBe(200); + const body = await response.text(); + const json = response.headers.get("Content-Type")?.includes("text/event-stream") + ? body + .split("\n") + .find((line) => line.startsWith("data: ")) + ?.slice(6) + : body; + expect(json).toBeTruthy(); + + return JSON.parse(json ?? "") as { + result: Record; + }; +}; + +const echoValidator = z.object({ message: z.string() }); +const echoTool: McpTool = { + name: "echo", + description: "Echo a message", + pathInRouter: ["echo"], + inputValidator: echoValidator, + inputSchema: z.toJSONSchema(echoValidator), +}; + +const createTestHandler = () => { + const echo = vi.fn(async (input: unknown) => input); + const handler = createMcpProtocolHandler({ + caller: { echo }, + tools: [echoTool], + version: "1.0.0", + instructions: "Test server", + formatToolError: () => "Tool failed", + }); + + return { echo, handler }; +}; + +describe("MCP v2 protocol", () => { + test("discovers the stateless 2026-07-28 server", async () => { + const { handler } = createTestHandler(); + const response = await parseResponse(await handler.fetch(createRequest("server/discover"))); + + expect(response.result.supportedVersions).toEqual(["2026-07-28"]); + expect(response.result).toMatchObject({ resultType: "complete", cacheScope: "private" }); + }); + + test("returns a deterministic, privately cacheable tool catalog", async () => { + const { handler } = createTestHandler(); + const response = await parseResponse(await handler.fetch(createRequest("tools/list"))); + + expect(response.result).toMatchObject({ + resultType: "complete", + ttlMs: 300_000, + cacheScope: "private", + tools: [{ name: "echo", description: "Echo a message" }], + }); + }); + + test("validates and calls Homarr procedures without a protocol session", async () => { + const { echo, handler } = createTestHandler(); + const response = await parseResponse( + await handler.fetch(createRequest("tools/call", { name: "echo", arguments: { message: "hello" } }, "echo")), + ); + + expect(echo).toHaveBeenCalledWith({ message: "hello" }); + expect(response.result.content).toEqual([{ type: "text", text: '{"message":"hello"}' }]); + }); + + test("keeps stateless compatibility with 2025 Streamable HTTP clients", async () => { + const { handler } = createTestHandler(); + const request = new Request("http://homarr.test/api/mcp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-03-26", + capabilities: {}, + clientInfo: { name: "legacy-test", version: "1.0.0" }, + }, + }), + }); + + const initializeResponse = await handler.fetch(request); + expect(initializeResponse.headers.has("Mcp-Session-Id")).toBe(false); + const response = await parseResponse(initializeResponse); + expect(response.result.protocolVersion).toBe("2025-03-26"); + + const listResponse = await handler.fetch( + new Request("http://homarr.test/api/mcp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + }, + body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }), + }), + ); + expect(listResponse.headers.has("Mcp-Session-Id")).toBe(false); + expect((await parseResponse(listResponse)).result.tools).toEqual([expect.objectContaining({ name: "echo" })]); + }); +}); diff --git a/apps/nextjs/src/app/api/mcp/_protocol.ts b/apps/nextjs/src/app/api/mcp/_protocol.ts new file mode 100644 index 000000000..fc17aba4e --- /dev/null +++ b/apps/nextjs/src/app/api/mcp/_protocol.ts @@ -0,0 +1,89 @@ +import { createMcpHandler, McpServer } from "@modelcontextprotocol/server"; + +import type { McpTool } from "@homarr/api/mcp"; + +interface CreateMcpProtocolHandlerOptions { + caller: unknown; + tools: McpTool[]; + version: string; + instructions: string; + configureServer?: (server: McpServer) => void; + formatToolError: (error: unknown) => string; + onToolError?: (toolName: string, error: unknown) => void; + onProtocolError?: (error: Error) => void; +} + +const resolveProcedure = (caller: unknown, path: string[]) => + path.reduce( + (current, segment) => + (typeof current === "object" || typeof current === "function") && current !== null + ? (current as Record)[segment] + : undefined, + caller, + ); + +export const createMcpProtocolHandler = ({ + caller, + tools, + version, + instructions, + configureServer, + formatToolError, + onToolError, + onProtocolError, +}: CreateMcpProtocolHandlerOptions) => + createMcpHandler( + () => { + const server = new McpServer( + { name: "homarr", version }, + { + capabilities: { tools: {} }, + instructions, + cacheHints: { + "tools/list": { ttlMs: 300_000, cacheScope: "private" }, + }, + }, + ); + + for (const tool of tools) { + server.registerTool( + tool.name, + { + description: tool.description, + inputSchema: tool.inputValidator, + }, + async (args) => { + try { + const procedure = resolveProcedure(caller, tool.pathInRouter); + if (typeof procedure !== "function") { + return { + content: [{ type: "text" as const, text: `Tool "${tool.name}" is not callable` }], + isError: true, + }; + } + + const input = Object.keys(args).length > 0 ? args : undefined; + const result = await (procedure as (value: unknown) => Promise)(input); + return { + content: [{ type: "text" as const, text: JSON.stringify(result) }], + }; + } catch (error) { + onToolError?.(tool.name, error); + return { + content: [{ type: "text" as const, text: JSON.stringify({ error: formatToolError(error) }) }], + isError: true, + }; + } + }, + ); + } + + configureServer?.(server); + + return server; + }, + { + legacy: "stateless", + onerror: onProtocolError, + }, + ); diff --git a/apps/nextjs/src/app/api/mcp/oauth/_store.ts b/apps/nextjs/src/app/api/mcp/oauth/_store.ts index 14352a41b..4cbd61def 100644 --- a/apps/nextjs/src/app/api/mcp/oauth/_store.ts +++ b/apps/nextjs/src/app/api/mcp/oauth/_store.ts @@ -15,6 +15,7 @@ interface AuthCode { codeChallenge: string; codeChallengeMethod: string; redirectUri: string; + resource: string | null; expiresAt: number; } @@ -24,6 +25,7 @@ interface PendingAuth { codeChallenge: string; codeChallengeMethod: string; state: string | null; + resource: string | null; expiresAt: number; } @@ -102,6 +104,7 @@ export function createAuthCode( codeChallenge: string, codeChallengeMethod: string, redirectUri: string, + resource: string | null, ) { if (authCodes.size >= MAX_AUTH_CODES) { return null; @@ -115,6 +118,7 @@ export function createAuthCode( codeChallenge, codeChallengeMethod, redirectUri, + resource, expiresAt: Date.now() + AUTH_CODE_TTL_MS, }); return code; diff --git a/apps/nextjs/src/app/api/mcp/oauth/authorize/route.ts b/apps/nextjs/src/app/api/mcp/oauth/authorize/route.ts index 927cdf60f..ec9974cb5 100644 --- a/apps/nextjs/src/app/api/mcp/oauth/authorize/route.ts +++ b/apps/nextjs/src/app/api/mcp/oauth/authorize/route.ts @@ -1,6 +1,7 @@ import type { NextRequest } from "next/server"; import { auth } from "@homarr/auth/next"; +import { extractBaseUrlFromHeaders } from "@homarr/common"; import { consumePendingAuth, createAuthCode, getClient, storePendingAuth } from "../_store"; @@ -37,6 +38,7 @@ export async function GET(req: NextRequest) { pending.codeChallenge, pending.codeChallengeMethod, pending.redirectUri, + pending.resource, ); if (!code) { return Response.json( @@ -59,6 +61,7 @@ export async function GET(req: NextRequest) { const codeChallenge = url.searchParams.get("code_challenge"); const codeChallengeMethod = url.searchParams.get("code_challenge_method") ?? "S256"; const state = url.searchParams.get("state"); + const resource = url.searchParams.get("resource"); if (!clientId || !redirectUri || !codeChallenge) { return Response.json( @@ -80,6 +83,17 @@ export async function GET(req: NextRequest) { ); } + const expectedResource = `${extractBaseUrlFromHeaders(req.headers)}/api/mcp`; + if (resource && resource !== expectedResource) { + return Response.json( + { + error: "invalid_target", + error_description: "The resource must be the canonical Homarr MCP endpoint", + }, + { status: 400 }, + ); + } + const client = getClient(clientId); if (!client) { return Response.json( @@ -103,7 +117,7 @@ export async function GET(req: NextRequest) { } if (session?.user) { - const code = createAuthCode(clientId, session.user.id, codeChallenge, codeChallengeMethod, redirectUri); + const code = createAuthCode(clientId, session.user.id, codeChallenge, codeChallengeMethod, redirectUri, resource); if (!code) { return Response.json( { @@ -126,6 +140,7 @@ export async function GET(req: NextRequest) { codeChallenge, codeChallengeMethod, state, + resource, }); if (!id) { return Response.json( diff --git a/apps/nextjs/src/app/api/mcp/oauth/token/route.ts b/apps/nextjs/src/app/api/mcp/oauth/token/route.ts index 1122e1950..895ec9520 100644 --- a/apps/nextjs/src/app/api/mcp/oauth/token/route.ts +++ b/apps/nextjs/src/app/api/mcp/oauth/token/route.ts @@ -80,7 +80,7 @@ export async function POST(req: Request) { ); } - const { grant_type, code, code_verifier, redirect_uri } = params; + const { grant_type, code, code_verifier, redirect_uri, resource } = params; if (grant_type !== "authorization_code") { return Response.json({ error: "unsupported_grant_type" }, { status: 400 }); @@ -111,6 +111,11 @@ export async function POST(req: Request) { return Response.json({ error: "invalid_grant", error_description: "redirect_uri mismatch" }, { status: 400 }); } + if (authCode.resource && resource !== authCode.resource) { + recordTokenFailure(ip); + return Response.json({ error: "invalid_target", error_description: "resource mismatch" }, { status: 400 }); + } + if (!verifyPkce(code_verifier, authCode.codeChallenge, authCode.codeChallengeMethod)) { recordTokenFailure(ip); return Response.json({ error: "invalid_grant", error_description: "PKCE verification failed" }, { status: 400 }); diff --git a/apps/nextjs/src/app/api/mcp/route.ts b/apps/nextjs/src/app/api/mcp/route.ts new file mode 100644 index 000000000..edc0a0d9d --- /dev/null +++ b/apps/nextjs/src/app/api/mcp/route.ts @@ -0,0 +1 @@ +export { handleMcpRequest as POST } from "./_handler"; diff --git a/packages/api/package.json b/packages/api/package.json index 4eeedbddf..15beaccc6 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -61,7 +61,6 @@ "react": "catalog:", "react-dom": "catalog:", "superjson": "catalog:", - "trpc-to-mcp": "catalog:", "trpc-to-openapi": "catalog:", "undici": "catalog:", "zod": "catalog:" diff --git a/packages/api/src/mcp-tools.ts b/packages/api/src/mcp-tools.ts new file mode 100644 index 000000000..b3e2d058e --- /dev/null +++ b/packages/api/src/mcp-tools.ts @@ -0,0 +1,83 @@ +import type { AnyRootTypes, Router, RouterRecord } from "@trpc/server/unstable-core-do-not-import"; +import { z } from "zod/v4"; + +export interface McpMeta { + mcp?: { + enabled: boolean; + name?: string; + description?: string; + }; +} + +export interface McpTool { + name: string; + description: string; + pathInRouter: string[]; + inputSchema: z.core.JSONSchema.JSONSchema; + inputValidator: z.ZodObject; +} + +const emptyInput = z.object({}); + +const mergeInputs = (inputs: z.ZodObject[]) => inputs.reduce((merged, input) => merged.extend(input.shape), emptyInput); + +const normalizeJsonSchema = (schema: z.core.JSONSchema.JSONSchema): z.core.JSONSchema.JSONSchema => { + const properties = (schema.properties ?? {}) as Record>; + const required = ((schema.required as string[] | undefined) ?? []).filter( + (key) => !("default" in (properties[key] ?? {})), + ); + + return { + ...schema, + type: "object", + properties, + required, + }; +}; + +export function extractMcpToolsFromProcedures( + router: Router, +): McpTool[] { + const tools: McpTool[] = []; + + for (const [path, procedure] of Object.entries(router["_def"].procedures)) { + const definition = procedure["_def"] as unknown as { + inputs?: z.ZodType[]; + meta?: McpMeta; + }; + const metadata = definition.meta?.mcp; + if (!metadata?.enabled) continue; + + const inputs = (definition.inputs ?? []).filter((input): input is z.ZodType => input !== undefined); + const procedureInput = + inputs.length === 0 + ? emptyInput + : inputs.length === 1 + ? (inputs[0] ?? emptyInput) + : inputs.every((input): input is z.ZodObject => input instanceof z.ZodObject) + ? mergeInputs(inputs) + : null; + + if ( + procedureInput === null || + (!(procedureInput instanceof z.ZodObject) && !procedureInput.safeParse(undefined).success) + ) { + console.warn(`[MCP] Procedure ${path} does not use an object input schema; using an empty schema`); + } + const inputValidator = procedureInput instanceof z.ZodObject ? procedureInput : emptyInput; + + tools.push({ + name: metadata.name ?? path.replaceAll(".", "_"), + description: metadata.description ?? "", + pathInRouter: path.split("."), + inputValidator, + inputSchema: normalizeJsonSchema( + z.toJSONSchema(inputValidator, { + unrepresentable: "any", + }), + ), + }); + } + + return tools.toSorted((left, right) => left.name.localeCompare(right.name)); +} diff --git a/packages/api/src/mcp.ts b/packages/api/src/mcp.ts index d31f50505..06d023863 100644 --- a/packages/api/src/mcp.ts +++ b/packages/api/src/mcp.ts @@ -1,4 +1,6 @@ export { createTRPCContext } from "./trpc"; +export { extractMcpToolsFromProcedures } from "./mcp-tools"; +export type { McpTool } from "./mcp-tools"; import { createTRPCRouter } from "./trpc"; import { appRouter as appRouterForApps } from "./router/app"; diff --git a/packages/api/src/router/test/widgets/custom-api-v2.spec.ts b/packages/api/src/router/test/widgets/custom-api-v2.spec.ts index a5e6f3c50..8bfc75319 100644 --- a/packages/api/src/router/test/widgets/custom-api-v2.spec.ts +++ b/packages/api/src/router/test/widgets/custom-api-v2.spec.ts @@ -84,7 +84,11 @@ const createSession = (userId: string): Session => ({ expires: new Date(Date.now() + 60_000).toISOString(), }); -async function setup(isPublic = true) { +async function setup( + isPublic = true, + customDefinition: HomarrCustomWidgetV2 = definition, + configuration: Record = {}, +) { const db = createDb(); const ownerId = createId(); const boardId = createId(); @@ -94,14 +98,14 @@ async function setup(isPublic = true) { await db.insert(boards).values({ id: boardId, name: createId(), creatorId: ownerId, isPublic }); await db.insert(customWidgetDefinitions).values({ id: definitionId, - ...serializeCustomWidgetDefinition(definition), + ...serializeCustomWidgetDefinition(customDefinition), creatorId: ownerId, }); await db.insert(items).values({ id: itemId, boardId, kind: "customApi", - options: stringifySuperJson({ definitionId, configuration: {}, configurationVersion: 1 }), + options: stringifySuperJson({ definitionId, configuration, configurationVersion: 1 }), }); return { db, ownerId, boardId, itemId, definitionId }; } @@ -122,6 +126,20 @@ describe("Custom JSX v2 board router", () => { expect(mocks.executeRequest).toHaveBeenCalledOnce(); }); + test("uses current defaults when an updated option no longer accepts the stored value", async () => { + const updatedDefinition: HomarrCustomWidgetV2 = { + ...definition, + options: { + limit: { label: "Limit", control: "slider", default: 5, min: 1, max: 10 }, + }, + template: "{options.limit}", + }; + const { db, itemId } = await setup(true, updatedDefinition, { limit: "5", removed: true }); + const caller = customApiRouter.createCaller({ db, deviceType: undefined, session: null }); + + await expect(caller.getData({ itemId })).resolves.toMatchObject({ options: { limit: 5 } }); + }); + test("hides private boards and disabled definitions before executing a request", async () => { const privateSetup = await setup(false); const anonymous = customApiRouter.createCaller({ diff --git a/packages/api/src/router/widgets/custom-api.ts b/packages/api/src/router/widgets/custom-api.ts index 15954da00..778788e58 100644 --- a/packages/api/src/router/widgets/custom-api.ts +++ b/packages/api/src/router/widgets/custom-api.ts @@ -9,6 +9,7 @@ import type { BoardPermission } from "@homarr/definitions"; import { getCustomWidgetConfirmation, getCustomWidgetDefaultOptions, + normalizeCustomWidgetOptions, validateCustomWidgetOptions, } from "@homarr/custom-widgets/core"; import type { CustomJsxRequest, CustomWidgetSource } from "@homarr/custom-widgets/core"; @@ -96,7 +97,10 @@ async function resolvePlacedDefinitionAsync(ctx: RouterContext, itemId: string) if (!stored.enabled) throw new TRPCError({ code: "FORBIDDEN", message: "Widget is disabled" }); const definition = parseStoredCustomWidgetDefinition(stored); - const configuration = { ...getCustomWidgetDefaultOptions(definition.options), ...itemOptions.configuration }; + const configuration = + itemOptions.configurationVersion === stored.updatedAt.getTime() + ? { ...getCustomWidgetDefaultOptions(definition.options), ...itemOptions.configuration } + : normalizeCustomWidgetOptions(definition.options, itemOptions.configuration); const issues = validateCustomWidgetOptions(definition.options, configuration); if (issues.length > 0) { throw new TRPCError({ diff --git a/packages/api/src/test/mcp.spec.ts b/packages/api/src/test/mcp.spec.ts index df29ce7f5..d379edee0 100644 --- a/packages/api/src/test/mcp.spec.ts +++ b/packages/api/src/test/mcp.spec.ts @@ -1,8 +1,9 @@ import type { TRPCError } from "@trpc/server"; import { describe, expect, test, vi } from "vitest"; -import { extractToolsFromProcedures } from "trpc-to-mcp"; +import { z } from "zod/v4"; import { mcpRouter } from "../mcp"; +import { extractMcpToolsFromProcedures } from "../mcp-tools"; vi.mock("@homarr/auth", () => ({})); @@ -125,7 +126,7 @@ function actualToolInventory() { string, { _def?: { type?: "query" | "mutation" } } >; - return extractToolsFromProcedures(mcpRouter) + return extractMcpToolsFromProcedures(mcpRouter) .map((tool) => { const procedure = procedures[tool.pathInRouter.join(".")]; const type = procedure?.["_def"]?.type; @@ -144,7 +145,7 @@ describe("production MCP router", () => { }); test("keeps every secret-bearing tool behind a mutation procedure", () => { - const tools = extractToolsFromProcedures(mcpRouter); + const tools = extractMcpToolsFromProcedures(mcpRouter); const procedures = mcpRouter["_def"].procedures as unknown as Record< string, { _def?: { type?: "query" | "mutation" } } @@ -158,7 +159,7 @@ describe("production MCP router", () => { }); test("gives every production tool a description", () => { - const tools = extractToolsFromProcedures(mcpRouter); + const tools = extractMcpToolsFromProcedures(mcpRouter); for (const tool of tools) expect(tool.description, `Tool ${tool.name} should have a description`).toBeTruthy(); expect(tools.find((tool) => tool.name === "customWidget_getAuthoringPrompt")?.description).toBe( "Get the current Custom Widget authoring instructions.", @@ -166,6 +167,14 @@ describe("production MCP router", () => { }); }); +test("MCP tools are deterministically ordered and retain executable schemas", () => { + const tools = extractMcpToolsFromProcedures(mcpRouter); + const toolNames = tools.map((tool) => tool.name); + + expect(toolNames).toEqual(toolNames.toSorted((left, right) => left.localeCompare(right))); + expect(tools.every((tool) => tool.inputValidator instanceof z.ZodObject)).toBe(true); +}); + describe("custom widget authoring procedure access", () => { const unauthenticatedCaller = mcpRouter.createCaller({ db: null as never, diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index b3fe7ae4d..2401a4650 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -8,7 +8,6 @@ */ import { initTRPC, TRPCError } from "@trpc/server"; import superjson from "superjson"; -import type { McpMeta } from "trpc-to-mcp"; import type { OpenApiMeta } from "trpc-to-openapi"; import { ZodError } from "zod/v4"; @@ -22,6 +21,7 @@ import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions"; import { env } from "./env"; import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries"; +import type { McpMeta } from "./mcp-tools"; const logger = createLogger({ module: "trpc" }); diff --git a/packages/custom-widgets/src/core/options.ts b/packages/custom-widgets/src/core/options.ts index b6ca003b0..0cc292cba 100644 --- a/packages/custom-widgets/src/core/options.ts +++ b/packages/custom-widgets/src/core/options.ts @@ -12,6 +12,18 @@ export function getCustomWidgetDefaultOptions( return Object.fromEntries(Object.entries(options).map(([name, option]) => [name, option.default])); } +export function normalizeCustomWidgetOptions( + options: CustomWidgetOptions, + value: Record, +): Record { + return Object.fromEntries( + Object.entries(options).map(([name, option]) => [ + name, + getCustomWidgetOptionValueIssue(option, value[name]) ? option.default : value[name], + ]), + ); +} + export function validateCustomWidgetOptions( options: CustomWidgetOptions, value: Record, diff --git a/packages/custom-widgets/src/test/options.spec.ts b/packages/custom-widgets/src/test/options.spec.ts index bd3eac0ef..078cfc62a 100644 --- a/packages/custom-widgets/src/test/options.spec.ts +++ b/packages/custom-widgets/src/test/options.spec.ts @@ -5,6 +5,7 @@ import { customWidgetOptionControls, customWidgetOptionSchema, getCustomWidgetDefaultOptions, + normalizeCustomWidgetOptions, validateCustomWidgetOptions, } from "../core"; @@ -69,6 +70,29 @@ describe("lean Custom Widget options", () => { expect(validateCustomWidgetOptions(options, configuration)).toEqual([]); }); + it("replaces stale values with current defaults", () => { + const options = customWidgetDefinitionSchema.parse({ + $schema: "homarr-custom-widget-v2", + name: "Updated options", + sources: {}, + requests: {}, + options: { + limit: { label: "Limit", control: "slider", default: 5, min: 1, max: 10 }, + label: { label: "Label", control: "text", default: "Default" }, + }, + template: "{options.limit}", + }).options; + + const normalized = normalizeCustomWidgetOptions(options, { + limit: "5", + label: "Custom", + removed: true, + }); + + expect(normalized).toEqual({ limit: 5, label: "Custom" }); + expect(validateCustomWidgetOptions(options, normalized)).toEqual([]); + }); + it("rejects duplicate static choice values", () => { expect( customWidgetOptionSchema.safeParse({ diff --git a/packages/widgets/src/_inputs/widget-custom-widget-configuration-input.tsx b/packages/widgets/src/_inputs/widget-custom-widget-configuration-input.tsx index 052563fee..619d8e0bc 100644 --- a/packages/widgets/src/_inputs/widget-custom-widget-configuration-input.tsx +++ b/packages/widgets/src/_inputs/widget-custom-widget-configuration-input.tsx @@ -21,7 +21,7 @@ import { IconAlertTriangle } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useOptionalBoard } from "@homarr/boards/context"; import type { CustomWidgetOption } from "@homarr/custom-widgets/core"; -import { validateCustomWidgetOptions } from "@homarr/custom-widgets/core"; +import { normalizeCustomWidgetOptions, validateCustomWidgetOptions } from "@homarr/custom-widgets/core"; import { CustomWidgetCodeEditor } from "@homarr/custom-widgets/workbench"; import type { CustomWidgetEditorMessages } from "@homarr/custom-widgets/workbench"; import { IconPicker } from "@homarr/forms-collection"; @@ -45,10 +45,23 @@ export const WidgetCustomWidgetConfigurationInput = ({ ); const definition = available.data?.find((candidate) => candidate.id === definitionId); const options = isRecord(definition?.options) ? (definition.options as Record) : null; - const issues = useMemo( - () => (options ? validateCustomWidgetOptions(options, configuration) : []), - [configuration, options], + const definitionVersion = definition?.updatedAt instanceof Date ? definition.updatedAt.getTime() : undefined; + const configurationVersion = form.values.options.configurationVersion; + const needsDefinitionRepair = definitionVersion !== undefined && configurationVersion !== definitionVersion; + const effectiveConfiguration = useMemo( + () => (options && needsDefinitionRepair ? normalizeCustomWidgetOptions(options, configuration) : configuration), + [configuration, needsDefinitionRepair, options], ); + const issues = useMemo( + () => (options ? validateCustomWidgetOptions(options, effectiveConfiguration) : []), + [effectiveConfiguration, options], + ); + + useEffect(() => { + if (!needsDefinitionRepair || definitionVersion === undefined) return; + form.setFieldValue(`options.${property}`, effectiveConfiguration); + form.setFieldValue("options.configurationVersion", definitionVersion); + }, [definitionVersion, effectiveConfiguration, form, needsDefinitionRepair, property]); useEffect(() => { const path = `options.${property}`; @@ -72,7 +85,7 @@ export const WidgetCustomWidgetConfigurationInput = ({ key={`${definitionId}:${name}`} option={option} path={`options.${property}.${name}`} - configuration={configuration} + configuration={effectiveConfiguration} definitionId={definitionId} /> ))} @@ -87,7 +100,7 @@ export const WidgetCustomWidgetConfigurationInput = ({ key={`${definitionId}:${name}`} option={option} path={`options.${property}.${name}`} - configuration={configuration} + configuration={effectiveConfiguration} definitionId={definitionId} /> ))} diff --git a/packages/widgets/src/advanced-focus-support.spec.ts b/packages/widgets/src/advanced-focus-support.spec.ts index 2ee1dcc84..5e69c39fb 100644 --- a/packages/widgets/src/advanced-focus-support.spec.ts +++ b/packages/widgets/src/advanced-focus-support.spec.ts @@ -3,41 +3,44 @@ import { describe, expect, test } from "vitest"; import { supportsAdvancedFocus } from "./definition"; import { widgetImports } from "."; -const compactOnlyWidgetKinds = [ - "app", - "bazarr", - "beszelAlerts", - "beszelSystemGrid", - "beszelSystemStats", - "beszelSystemTable", - "coolify", - "dnsHoleControls", - "dnsHoleSummary", - "iframe", - "indexerManager", - "mediaRequests-requestList", - "mediaRequests-requestStats", - "minecraftServerStatus", - "networkControllerSummary", - "notebook", - "notifications", - "paperlessNgx", - "patchmon", - "releases", - "rssFeed", - "stockPrice", - "systemDisks", - "video", - "vpn", +const advancedFocusWidgetKinds = [ + "anchorNote", + "archiveTeamWarrior", + "audioStats", + "bookmarks", + "calendar", + "clock", + "dockerContainers", + "downloads", + "firewall", + "healthMonitoring", + "immich-albumCarousel", + "immich-serverStats", + "mediaMissing", + "mediaReleases", + "mediaServer", + "mediaTranscoding", + "networkControllerStatus", + "smartHome-entityState", + "smartHome-executeAutomation", + "speedtestTracker", + "systemResources", + "timetable", + "tracearr", + "traefik", + "umami", + "ups", + "uptimeKuma", + "weather", ] as const; describe("advanced focus support", () => { test("is reserved for widgets with a distinct advanced experience", () => { - const actualCompactOnlyKinds = Object.entries(widgetImports) - .filter(([, widgetImport]) => !supportsAdvancedFocus(widgetImport.definition)) + const actualAdvancedFocusKinds = Object.entries(widgetImports) + .filter(([, widgetImport]) => supportsAdvancedFocus(widgetImport.definition)) .map(([kind]) => kind) .toSorted(); - expect(actualCompactOnlyKinds).toEqual([...compactOnlyWidgetKinds].toSorted()); + expect(actualAdvancedFocusKinds).toEqual([...advancedFocusWidgetKinds].toSorted()); }); }); diff --git a/packages/widgets/src/anchor-note/index.ts b/packages/widgets/src/anchor-note/index.ts index 9baaccf2d..4eac1a441 100644 --- a/packages/widgets/src/anchor-note/index.ts +++ b/packages/widgets/src/anchor-note/index.ts @@ -5,6 +5,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("anchorNote", { icon: IconNotes, + supportsAdvancedFocus: true, queryKey: [["widget", "anchorNotes"]], maxIntegrations: 1, supportedIntegrations: ["anchor"], diff --git a/packages/widgets/src/archive-team-warrior/index.ts b/packages/widgets/src/archive-team-warrior/index.ts index 1eaf0a691..38a3eef36 100644 --- a/packages/widgets/src/archive-team-warrior/index.ts +++ b/packages/widgets/src/archive-team-warrior/index.ts @@ -5,6 +5,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("archiveTeamWarrior", { icon: IconArchive, + supportsAdvancedFocus: true, maxIntegrations: 1, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/assistant/definition.spec.ts b/packages/widgets/src/assistant/definition.spec.ts index d0093e139..d97b51346 100644 --- a/packages/widgets/src/assistant/definition.spec.ts +++ b/packages/widgets/src/assistant/definition.spec.ts @@ -1,8 +1,13 @@ import { describe, expect, it } from "vitest"; +import { supportsAdvancedFocus } from "../definition"; import { definition } from "."; describe("assistant widget definition", () => { + it("stays compact without an explicit advanced-focus opt-in", () => { + expect(supportsAdvancedFocus(definition)).toBe(false); + }); + it("follows the current conversation by default", () => { const options = definition.createOptions(); diff --git a/packages/widgets/src/audio-stats/index.ts b/packages/widgets/src/audio-stats/index.ts index a21e70278..6fb3b2014 100644 --- a/packages/widgets/src/audio-stats/index.ts +++ b/packages/widgets/src/audio-stats/index.ts @@ -13,6 +13,7 @@ const hideUnlessAudiobookshelf = { export const { definition, componentLoader } = createWidgetDefinition("audioStats", { icon: IconHeadphones, + supportsAdvancedFocus: true, queryKeys: [[["widget", "audioStats", "getStats"]], [["widget", "mediaServer", "getCurrentStreams"]]], queryMatcher: matchesWidgetRuntimeQuery, supportedIntegrations: ["navidrome", "audiobookshelf"], diff --git a/packages/widgets/src/bookmarks/index.tsx b/packages/widgets/src/bookmarks/index.tsx index c18ff796e..85fc442cf 100644 --- a/packages/widgets/src/bookmarks/index.tsx +++ b/packages/widgets/src/bookmarks/index.tsx @@ -10,6 +10,7 @@ import { BookmarkAddButton } from "./add-button"; export const { definition, componentLoader } = createWidgetDefinition("bookmarks", { icon: IconBookmark, + supportsAdvancedFocus: true, queryKey: [["app", "byIds"]], createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/calendar/index.ts b/packages/widgets/src/calendar/index.ts index 270981f5d..471c50f17 100644 --- a/packages/widgets/src/calendar/index.ts +++ b/packages/widgets/src/calendar/index.ts @@ -9,6 +9,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("calendar", { icon: IconCalendar, + supportsAdvancedFocus: true, queryKey: [["widget", "calendar", "findAllEvents"]], queryMatcher: matchesWidgetRuntimeQuery, createOptions() { diff --git a/packages/widgets/src/clock/index.ts b/packages/widgets/src/clock/index.ts index e7657b918..45b040009 100644 --- a/packages/widgets/src/clock/index.ts +++ b/packages/widgets/src/clock/index.ts @@ -6,6 +6,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("clock", { icon: IconClock, + supportsAdvancedFocus: true, queryKey: [["widget", "weather", "atLocation"]], queryMatcher: ({ input }, scope) => { const location = scope.options.weatherLocation; diff --git a/packages/widgets/src/definition.spec.ts b/packages/widgets/src/definition.spec.ts index 7f976692c..215c884ef 100644 --- a/packages/widgets/src/definition.spec.ts +++ b/packages/widgets/src/definition.spec.ts @@ -2,7 +2,9 @@ import { describe, expect, test } from "vitest"; import { definition as appDefinition } from "./app"; import { definition as beszelSystemGridDefinition } from "./beszel-system-grid"; import { definition as beszelSystemTableDefinition } from "./beszel-system-table"; +import { definition as customApiDefinition } from "./custom-api"; import { getWidgetQueryKeys, supportsAdvancedFocus } from "./definition"; +import { definition as downloadsDefinition } from "./downloads"; describe("getWidgetQueryKeys", () => { test("falls back to the widget router namespace", () => { @@ -24,10 +26,15 @@ describe("getWidgetQueryKeys", () => { }); describe("supportsAdvancedFocus", () => { - test.each([appDefinition, beszelSystemGridDefinition, beszelSystemTableDefinition])( + test.each([appDefinition, beszelSystemGridDefinition, beszelSystemTableDefinition, customApiDefinition])( "keeps non-enhanced widgets compact", (widgetDefinition) => { expect(supportsAdvancedFocus(widgetDefinition)).toBe(false); }, ); + + test("requires an explicit opt-in", () => { + expect(supportsAdvancedFocus({})).toBe(false); + expect(supportsAdvancedFocus(downloadsDefinition)).toBe(true); + }); }); diff --git a/packages/widgets/src/definition.ts b/packages/widgets/src/definition.ts index 3f148ce77..6ef443999 100644 --- a/packages/widgets/src/definition.ts +++ b/packages/widgets/src/definition.ts @@ -127,7 +127,7 @@ export interface WidgetDefinition { } export const supportsAdvancedFocus = (definition: object) => - !("supportsAdvancedFocus" in definition) || definition.supportsAdvancedFocus !== false; + "supportsAdvancedFocus" in definition && definition.supportsAdvancedFocus === true; export const getWidgetQueryKeys = ( definition: { diff --git a/packages/widgets/src/docker/index.ts b/packages/widgets/src/docker/index.ts index bc229fed5..2ca930913 100644 --- a/packages/widgets/src/docker/index.ts +++ b/packages/widgets/src/docker/index.ts @@ -25,6 +25,7 @@ const columnTranslationKeyMap = { export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", { icon: IconBrandDocker, + supportsAdvancedFocus: true, queryKey: [["docker", "getContainers"]], refetchInterval: 30, createOptions() { diff --git a/packages/widgets/src/downloads/index.ts b/packages/widgets/src/downloads/index.ts index 60064de9a..7ac9d1182 100644 --- a/packages/widgets/src/downloads/index.ts +++ b/packages/widgets/src/downloads/index.ts @@ -42,6 +42,7 @@ const sortColumns = [ export const { definition, componentLoader } = createWidgetDefinition("downloads", { icon: IconDownload, + supportsAdvancedFocus: true, queryKey: [["widget", "downloads", "getJobsAndStatuses"]], queryMatcher: ({ input }, scope) => widgetQueryInputMatches(input, { diff --git a/packages/widgets/src/firewall/index.ts b/packages/widgets/src/firewall/index.ts index e154fd3ac..2a6b24854 100644 --- a/packages/widgets/src/firewall/index.ts +++ b/packages/widgets/src/firewall/index.ts @@ -7,6 +7,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("firewall", { icon: IconWall, + supportsAdvancedFocus: true, refetchInterval: 5, createOptions() { return optionsBuilder.from(() => ({})); diff --git a/packages/widgets/src/health-monitoring/index.ts b/packages/widgets/src/health-monitoring/index.ts index c322e322a..1b2e84982 100644 --- a/packages/widgets/src/health-monitoring/index.ts +++ b/packages/widgets/src/health-monitoring/index.ts @@ -8,6 +8,7 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", { icon: IconHeartRateMonitor, + supportsAdvancedFocus: true, queryKeys: [[["integration", "byIds"]], [["widget", "healthMonitoring"]]], refetchInterval: 5, createOptions() { diff --git a/packages/widgets/src/immich/album-carousel/index.ts b/packages/widgets/src/immich/album-carousel/index.ts index f18585280..34eb0e667 100644 --- a/packages/widgets/src/immich/album-carousel/index.ts +++ b/packages/widgets/src/immich/album-carousel/index.ts @@ -51,6 +51,7 @@ const createOptions = () => export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", { icon: IconPhoto, + supportsAdvancedFocus: true, queryKey: [["widget", "immich", "getAlbum"]], queryMatcher: ({ input }, scope) => widgetQueryInputMatches(input, { diff --git a/packages/widgets/src/immich/server-stats/index.ts b/packages/widgets/src/immich/server-stats/index.ts index a1c966664..dedd420fe 100644 --- a/packages/widgets/src/immich/server-stats/index.ts +++ b/packages/widgets/src/immich/server-stats/index.ts @@ -5,6 +5,7 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("immich-serverStats", { icon: IconGraphFilled, + supportsAdvancedFocus: true, queryKeys: [[["widget", "immich", "getServerStats"]], [["widget", "immich", "getAlbums"]]], queryMatcher: matchesWidgetRuntimeQuery, refetchInterval: null, diff --git a/packages/widgets/src/media-missing/index.ts b/packages/widgets/src/media-missing/index.ts index 69a6ecf73..38ac8cd68 100644 --- a/packages/widgets/src/media-missing/index.ts +++ b/packages/widgets/src/media-missing/index.ts @@ -9,6 +9,7 @@ const pageSizeOptions = ["10", "20", "30", "50"] as const; export const { componentLoader, definition } = createWidgetDefinition("mediaMissing", { icon: IconMovie, + supportsAdvancedFocus: true, queryKey: [["widget", "mediaOrganizer", "getData"]], queryMatcher: matchesWidgetRuntimeQuery, createOptions() { diff --git a/packages/widgets/src/media-releases/index.ts b/packages/widgets/src/media-releases/index.ts index 3e6cccbe5..55eff84e5 100644 --- a/packages/widgets/src/media-releases/index.ts +++ b/packages/widgets/src/media-releases/index.ts @@ -5,6 +5,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("mediaReleases", { icon: IconTicket, + supportsAdvancedFocus: true, queryKey: [["widget", "mediaRelease"]], refetchInterval: null, createOptions() { diff --git a/packages/widgets/src/media-server/index.ts b/packages/widgets/src/media-server/index.ts index 46f3fd489..559b029ee 100644 --- a/packages/widgets/src/media-server/index.ts +++ b/packages/widgets/src/media-server/index.ts @@ -7,6 +7,7 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("mediaServer", { icon: IconVideo, + supportsAdvancedFocus: true, queryKey: [["widget", "mediaServer", "getCurrentStreams"]], queryMatcher: ({ input }, scope) => widgetQueryInputMatches(input, { diff --git a/packages/widgets/src/media-transcoding/index.ts b/packages/widgets/src/media-transcoding/index.ts index fc91622e3..bff9c1ea3 100644 --- a/packages/widgets/src/media-transcoding/index.ts +++ b/packages/widgets/src/media-transcoding/index.ts @@ -11,6 +11,7 @@ export const views = ["workers", "queue", "statistics"] as const; export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", { icon: IconTransform, + supportsAdvancedFocus: true, queryKey: [["widget", "mediaTranscoding", "getDataAsync"]], queryMatcher: matchesWidgetRuntimeQuery, maxIntegrations: 1, diff --git a/packages/widgets/src/network-controller/network-status/index.ts b/packages/widgets/src/network-controller/network-status/index.ts index 3ab66da82..e3741d8e1 100644 --- a/packages/widgets/src/network-controller/network-status/index.ts +++ b/packages/widgets/src/network-controller/network-status/index.ts @@ -7,6 +7,7 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("networkControllerStatus", { icon: IconTopologyFull, + supportsAdvancedFocus: true, queryKey: [["widget", "networkController"]], refetchInterval: null, createOptions() { diff --git a/packages/widgets/src/smart-home/entity-state/index.ts b/packages/widgets/src/smart-home/entity-state/index.ts index 93a12a872..551fa42de 100644 --- a/packages/widgets/src/smart-home/entity-state/index.ts +++ b/packages/widgets/src/smart-home/entity-state/index.ts @@ -7,6 +7,7 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", { icon: IconBinaryTree, + supportsAdvancedFocus: true, queryKey: [["widget", "smartHome"]], maxIntegrations: 1, createOptions() { diff --git a/packages/widgets/src/smart-home/execute-automation/index.ts b/packages/widgets/src/smart-home/execute-automation/index.ts index e4435f892..2a349a84e 100644 --- a/packages/widgets/src/smart-home/execute-automation/index.ts +++ b/packages/widgets/src/smart-home/execute-automation/index.ts @@ -7,6 +7,7 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("smartHome-executeAutomation", { icon: IconBinaryTree, + supportsAdvancedFocus: true, maxIntegrations: 1, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/speedtest-tracker/index.ts b/packages/widgets/src/speedtest-tracker/index.ts index 14cc5d118..7de41100f 100644 --- a/packages/widgets/src/speedtest-tracker/index.ts +++ b/packages/widgets/src/speedtest-tracker/index.ts @@ -5,6 +5,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", { icon: IconSpeedboat, + supportsAdvancedFocus: true, refetchInterval: null, createOptions() { return optionsBuilder.from( diff --git a/packages/widgets/src/system-resources/index.ts b/packages/widgets/src/system-resources/index.ts index b35bab42f..7c0e84fdc 100644 --- a/packages/widgets/src/system-resources/index.ts +++ b/packages/widgets/src/system-resources/index.ts @@ -14,6 +14,7 @@ const labelDisplayModeOptions = { export const { definition, componentLoader } = createWidgetDefinition("systemResources", { icon: IconGraphFilled, + supportsAdvancedFocus: true, queryKey: [["widget", "healthMonitoring"]], refetchInterval: 5, supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"], diff --git a/packages/widgets/src/timetable/index.ts b/packages/widgets/src/timetable/index.ts index 81ec81db8..acd3f703d 100644 --- a/packages/widgets/src/timetable/index.ts +++ b/packages/widgets/src/timetable/index.ts @@ -9,6 +9,7 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("timetable", { icon: IconBusStop, + supportsAdvancedFocus: true, errors: { BAD_REQUEST: { icon: IconWorldOff, diff --git a/packages/widgets/src/tracearr/index.ts b/packages/widgets/src/tracearr/index.ts index 9c34eb548..13851a846 100644 --- a/packages/widgets/src/tracearr/index.ts +++ b/packages/widgets/src/tracearr/index.ts @@ -5,6 +5,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("tracearr", { icon: IconActivityHeartbeat, + supportsAdvancedFocus: true, refetchInterval: 5, createOptions() { return optionsBuilder.from((factory) => ({ diff --git a/packages/widgets/src/traefik/index.ts b/packages/widgets/src/traefik/index.ts index 854075121..95392a152 100644 --- a/packages/widgets/src/traefik/index.ts +++ b/packages/widgets/src/traefik/index.ts @@ -5,6 +5,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("traefik", { icon: IconRoute, + supportsAdvancedFocus: true, createOptions() { return optionsBuilder.from((factory) => ({ showTcp: factory.switch({ diff --git a/packages/widgets/src/umami/index.ts b/packages/widgets/src/umami/index.ts index 423eebebe..9c96e4a8e 100644 --- a/packages/widgets/src/umami/index.ts +++ b/packages/widgets/src/umami/index.ts @@ -8,6 +8,7 @@ export const timeFrameValues = ["today", "24h", "7d", "30d", "month", "lastMonth export const { definition, componentLoader } = createWidgetDefinition("umami", { icon: IconChartBar, + supportsAdvancedFocus: true, queryKeys: [ [["widget", "umami", "getVisitorStats"]], [["widget", "umami", "getActiveVisitors"]], diff --git a/packages/widgets/src/ups/index.ts b/packages/widgets/src/ups/index.ts index 681daf417..0ee8c70d5 100644 --- a/packages/widgets/src/ups/index.ts +++ b/packages/widgets/src/ups/index.ts @@ -7,6 +7,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("ups", { icon: IconBatteryCharging, + supportsAdvancedFocus: true, createOptions() { return optionsBuilder.from((factory) => ({ showBattery: factory.switch({ diff --git a/packages/widgets/src/uptime-kuma/index.ts b/packages/widgets/src/uptime-kuma/index.ts index b9eebcd8c..0b0cff5ad 100644 --- a/packages/widgets/src/uptime-kuma/index.ts +++ b/packages/widgets/src/uptime-kuma/index.ts @@ -5,6 +5,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("uptimeKuma", { icon: IconHeartbeat, + supportsAdvancedFocus: true, createOptions() { return optionsBuilder.from((factory) => ({ showAverageUptime: factory.switch({ diff --git a/packages/widgets/src/weather/index.ts b/packages/widgets/src/weather/index.ts index 978c9a855..11ced3b39 100644 --- a/packages/widgets/src/weather/index.ts +++ b/packages/widgets/src/weather/index.ts @@ -7,6 +7,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("weather", { icon: IconCloud, + supportsAdvancedFocus: true, queryKey: [["widget", "weather", "atLocation"]], queryMatcher: ({ input }, scope) => { const location = scope.options.location; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e37933c2..16e1e839b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,9 +144,9 @@ catalogs: '@mantine/tiptap': specifier: ^9.4.1 version: 9.4.1 - '@modelcontextprotocol/sdk': - specifier: 1.29.0 - version: 1.29.0 + '@modelcontextprotocol/server': + specifier: 2.0.0 + version: 2.0.0 '@paralleldrive/cuid2': specifier: ^3.3.0 version: 3.3.0 @@ -489,9 +489,6 @@ catalogs: maria2: specifier: ^0.4.1 version: 0.4.1 - mcp-handler: - specifier: ^1.1.0 - version: 1.1.0 mermaid: specifier: ^11.15.0 version: 11.15.0 @@ -606,9 +603,6 @@ catalogs: timezones-ical-library: specifier: ^2.2.1 version: 2.2.1 - trpc-to-mcp: - specifier: ^1.3.2 - version: 1.3.2 trpc-to-openapi: specifier: ^3.3.0 version: 3.3.0 @@ -1104,9 +1098,9 @@ importers: '@mantine/tiptap': specifier: 'catalog:' version: 9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tiptap/extension-link@3.27.3(@tiptap/core@3.27.3(@tiptap/pm@3.27.3))(@tiptap/pm@3.27.3))(@tiptap/react@3.27.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.3(@tiptap/pm@3.27.3))(@tiptap/pm@3.27.3)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@modelcontextprotocol/sdk': + '@modelcontextprotocol/server': specifier: 'catalog:' - version: 1.29.0(supports-color@10.2.2)(zod@4.4.3) + version: 2.0.0 '@scalar/api-reference-react': specifier: 'catalog:' version: 0.9.54(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2)(zod@4.4.3) @@ -1200,9 +1194,6 @@ importers: mantine-react-table: specifier: 'catalog:' version: mantine-react-table-open@9.0.3(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/dates@9.4.1(@mantine/core@9.4.1(@mantine/hooks@9.4.1(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mantine/hooks@9.4.1(react@19.2.7))(@tabler/icons-react@3.44.0(react@19.2.7))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - mcp-handler: - specifier: 'catalog:' - version: 1.1.0(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.4.3))(next@16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) mermaid: specifier: 'catalog:' version: 11.15.0 @@ -1248,9 +1239,6 @@ importers: superjson: specifier: 'catalog:' version: 2.2.6 - trpc-to-mcp: - specifier: 'catalog:' - version: 1.3.2(7c45d4095abf7cef4938a6fc2cc62863) use-deep-compare-effect: specifier: 'catalog:' version: 1.8.1(react@19.2.7) @@ -1558,9 +1546,6 @@ importers: superjson: specifier: 'catalog:' version: 2.2.6 - trpc-to-mcp: - specifier: 'catalog:' - version: 1.3.2(09f4f932022b61bbb1db30a9d75546c7) trpc-to-openapi: specifier: 'catalog:' version: 3.3.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.18.0(typescript@7.0.2))(zod-openapi@5.4.6(zod@4.4.3))(zod@4.4.3) @@ -3985,85 +3970,6 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@better-auth/core@1.6.15': - resolution: {integrity: sha512-gPgeEn+2t2cohSKj1CJFCJLo4jNqvtvhKP/y6Uwuurg4ReEf2Y9PUCYffvbp7DDc+GevWTuD0Cv9x0+e83+HpA==} - peerDependencies: - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.1.21 - '@cloudflare/workers-types': '>=4' - '@opentelemetry/api': ^1.9.0 - better-call: 1.3.5 - jose: ^6.1.0 - kysely: ^0.28.5 || ^0.29.0 - nanostores: ^1.0.1 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - '@opentelemetry/api': - optional: true - - '@better-auth/drizzle-adapter@1.6.15': - resolution: {integrity: sha512-+ho2RozN6cZmCN+6XkZd/ec5iolVlefgInQ7znJ18qRPbgllHxdyu9tGrgKZyEsXSCyqAa6wi5Yb4hd3WZmTNQ==} - peerDependencies: - '@better-auth/core': ^1.6.15 - '@better-auth/utils': 0.4.1 - drizzle-orm: ^0.45.2 - peerDependenciesMeta: - drizzle-orm: - optional: true - - '@better-auth/kysely-adapter@1.6.15': - resolution: {integrity: sha512-E29Sugm+DWRK4oQNBPrjPA4kBYiKy2bBtX1arIlkuyAB9A1BEb4Xn+8t7wlNIF5eFcxpkFZ3F80ceSpWDSYU0Q==} - peerDependencies: - '@better-auth/core': ^1.6.15 - '@better-auth/utils': 0.4.1 - kysely: ^0.28.17 || ^0.29.0 - peerDependenciesMeta: - kysely: - optional: true - - '@better-auth/memory-adapter@1.6.15': - resolution: {integrity: sha512-BRE5Ft0Tn5thOPjCmyXHge7kOI/paLybaKDU+Hzg24DZMU6JvWooYWzvf52r1viDbxq5tz0G97iyaH4bouDyug==} - peerDependencies: - '@better-auth/core': ^1.6.15 - '@better-auth/utils': 0.4.1 - - '@better-auth/mongo-adapter@1.6.15': - resolution: {integrity: sha512-r0X9AtFhwDeOU1KMP9kZ5NeV0O3TSFzn8+uidaH+aicy2e1dNUsd9nPYrozEloSxdA2ZOnv5gD4jzH7HPeKkUQ==} - peerDependencies: - '@better-auth/core': ^1.6.15 - '@better-auth/utils': 0.4.1 - mongodb: ^6.0.0 || ^7.0.0 - peerDependenciesMeta: - mongodb: - optional: true - - '@better-auth/prisma-adapter@1.6.15': - resolution: {integrity: sha512-pUOlIUnpMu2l8C2ytgu46+OoOr3LZ+aLpJMFRGIPdnWl1BF2co2YaoZPT3+ZBu1Lzm0FrBN1ZRCAzZbkFoeIAQ==} - peerDependencies: - '@better-auth/core': ^1.6.15 - '@better-auth/utils': 0.4.1 - '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 - prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 - peerDependenciesMeta: - '@prisma/client': - optional: true - prisma: - optional: true - - '@better-auth/telemetry@1.6.15': - resolution: {integrity: sha512-eoFlUPVrVXLML9saHD11OSKKSRCAYETgRgBW4vQF3Y6pCgmThKD9oehYk3kkb/FsRgfqmsocmvAFqsiyjPIygg==} - peerDependencies: - '@better-auth/core': ^1.6.15 - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.1.21 - - '@better-auth/utils@0.4.1': - resolution: {integrity: sha512-SZBPRPF3z0nBvE5ygOkxae35wnnXPRShmqFo78S+qslLeFoPu/pMgnXAuNKFMMybac3tiLaVg1e3MQW5MC+1iA==} - - '@better-fetch/fetch@1.1.21': - resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} - '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -5548,12 +5454,6 @@ packages: resolution: {integrity: sha512-Nsh23+aIMzWhHelM7XDGz5B+5ADprOWuPVUyCpEStOgnHQfdC1Wa6z66VkROBeV2ep4a7YgvQV166/OCzP1sgw==} engines: {node: '>=22.0.0'} - '@hono/node-server@1.19.14': - resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: '>=4.12.28' - '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -6299,25 +6199,13 @@ packages: '@mermaid-js/parser@1.1.1': resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} - '@modelcontextprotocol/sdk@1.26.0': - resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true + '@modelcontextprotocol/core@2.0.0': + resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==} + engines: {node: '>=20'} - '@modelcontextprotocol/sdk@1.29.0': - resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true + '@modelcontextprotocol/server@2.0.0': + resolution: {integrity: sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==} + engines: {node: '>=20'} '@module-federation/error-codes@0.22.0': resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} @@ -6458,10 +6346,6 @@ packages: peerDependencies: react: ^16.14 || ^17.0 || ^18.0 || ^19.0 - '@noble/ciphers@2.2.0': - resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==} - engines: {node: '>= 20.19.0'} - '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -6614,10 +6498,6 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/semantic-conventions@1.41.1': - resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} - engines: {node: '>=14'} - '@oxc-project/types@0.138.0': resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} @@ -7943,35 +7823,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/client@1.6.1': - resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} - engines: {node: '>=14'} - - '@redis/graph@1.1.1': - resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/json@1.0.7': - resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/search@1.2.0': - resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/time-series@1.1.0': - resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} - peerDependencies: - '@redis/client': ^1.0.0 - '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -9911,10 +9762,6 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-import-phases@1.0.4: resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} engines: {node: '>=10.13.0'} @@ -10268,76 +10115,6 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - better-auth@1.6.15: - resolution: {integrity: sha512-0nuQuEru3ZrLF+9xFUuN3llAmR+6gHLtLunoXaZxB9lXGjSmfBcc6SZUgYq4DfzugPnLvdnzYazsyprZFSFC4Q==} - peerDependencies: - '@lynx-js/react': '*' - '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 - '@sveltejs/kit': ^2.0.0 - '@tanstack/react-start': ^1.0.0 - '@tanstack/solid-start': ^1.0.0 - better-sqlite3: ^12.0.0 - drizzle-kit: '>=0.31.4' - drizzle-orm: ^0.45.2 - mongodb: ^6.0.0 || ^7.0.0 - mysql2: ^3.0.0 - next: ^14.0.0 || ^15.0.0 || ^16.0.0 - pg: ^8.0.0 - prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - solid-js: ^1.0.0 - svelte: ^4.0.0 || ^5.0.0 - vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 - vue: ^3.0.0 - peerDependenciesMeta: - '@lynx-js/react': - optional: true - '@prisma/client': - optional: true - '@sveltejs/kit': - optional: true - '@tanstack/react-start': - optional: true - '@tanstack/solid-start': - optional: true - better-sqlite3: - optional: true - drizzle-kit: - optional: true - drizzle-orm: - optional: true - mongodb: - optional: true - mysql2: - optional: true - next: - optional: true - pg: - optional: true - prisma: - optional: true - react: - optional: true - react-dom: - optional: true - solid-js: - optional: true - svelte: - optional: true - vitest: - optional: true - vue: - optional: true - - better-call@1.3.5: - resolution: {integrity: sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==} - peerDependencies: - zod: ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - better-sqlite3@12.11.1: resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} @@ -10365,10 +10142,6 @@ packages: resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} - bonjour-service@1.4.0: resolution: {integrity: sha512-fGQtj1qdR9vIKjFiWPQd52qIqwjaYqhcI40JEiDuvlZ86E7ZBPBwY9fPgHy9r2rYGIjiRfctNPYz6OQU73ww2w==} @@ -10618,10 +10391,6 @@ packages: resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - cmdk@1.1.1: resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} peerDependencies: @@ -10688,10 +10457,6 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} - commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} - commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -10752,18 +10517,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} - conventional-changelog-angular@8.0.0: resolution: {integrity: sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA==} engines: {node: '>=18'} @@ -10800,10 +10557,6 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} - 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'} @@ -10843,10 +10596,6 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -11801,10 +11550,6 @@ packages: resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - execa@10.0.1: resolution: {integrity: sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==} engines: {node: '>=22'} @@ -11829,20 +11574,10 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.5.2: - resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - express@4.22.2: resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} engines: {node: '>= 0.10.0'} - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -11967,10 +11702,6 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - find-cache-dir@4.0.0: resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} engines: {node: '>=14.16'} @@ -12046,10 +11777,6 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -12089,10 +11816,6 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -12346,10 +12069,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.12.24: - resolution: {integrity: sha512-I36D1s+HgQc55KbhEr4iybfxv/9o1zdpw+XEM6dJa91LqQD0HCoSGdxpRJCZE+aavs87j4V3Ls2OJzq8C/U4iw==} - engines: {node: '>=16.9.0'} - hook-std@4.0.0: resolution: {integrity: sha512-IHI4bEVOt3vRUDJ+bFA9VUJlo7SzvFARPNLw75pqSmAOP2HmTWfFJtPvLBrDrlgjEYXY9zs7SFdHPQaJShkSCQ==} engines: {node: '>=20'} @@ -12622,10 +12341,6 @@ packages: resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} - engines: {node: '>= 12'} - ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -12759,9 +12474,6 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} @@ -12966,9 +12678,6 @@ packages: 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==} @@ -13319,16 +13028,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mcp-handler@1.1.0: - resolution: {integrity: sha512-MVCES7g18gcoZy+R/3v5nadkUMzMAWdos8jRl6DyljOKvd2/ZKDmwlCjL6zp4vo+7FeCXOYL1uWinHWlkKAAUg==} - hasBin: true - peerDependencies: - '@modelcontextprotocol/sdk': 1.26.0 - next: '>=13.0.0' - peerDependenciesMeta: - next: - optional: true - mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} @@ -13402,10 +13101,6 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - medium-zoom@1.1.0: resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} @@ -13421,10 +13116,6 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -13726,10 +13417,6 @@ packages: engines: {node: ^22 || ^24 || >=26} hasBin: true - nanostores@1.3.0: - resolution: {integrity: sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==} - engines: {node: ^20.0.0 || >=22.0.0} - napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} @@ -14267,9 +13954,6 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -15051,10 +14735,6 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} @@ -15294,9 +14974,6 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - redis@4.7.1: - resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} - redux-thunk@3.1.0: resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: @@ -15484,16 +15161,9 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - rou3@0.7.12: - resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} - 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'} - rtlcss@4.3.0: resolution: {integrity: sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==} engines: {node: '>=12.0.0'} @@ -15605,10 +15275,6 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -15633,10 +15299,6 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -16305,14 +15967,6 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - trpc-to-mcp@1.3.2: - resolution: {integrity: sha512-e2pF9ABqIWnJXa8lNlJeC8anfzubH5WbQ1gVcUgjBrEDjeKH2FNo7xhqil0j4GZp+zB2S1VBQgsowGlp4E3qfw==} - peerDependencies: - '@trpc/server': ^11.4.4 - better-auth: ^1.3.4 - mcp-handler: ^1.0.1 - zod: ^4.0.15 - trpc-to-openapi@3.3.0: resolution: {integrity: sha512-PNMXq0K37Ott5ysNACYN1XLaM5xZGNJrx1QiQVTlk4hmgJomoyiAKhVrpwrK02JW3joSQmHYBOZ7pK6Hdioekw==} peerDependencies: @@ -16402,10 +16056,6 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} @@ -17057,9 +16707,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -17123,11 +16770,6 @@ packages: peerDependencies: zod: ^3.25.74 || ^4.0.0 - zod-to-json-schema@3.25.2: - resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} - peerDependencies: - zod: ^3.25.28 || ^4 - zod-validation-error@5.0.0: resolution: {integrity: sha512-hmk+pkyKq7Q71PiWVSDUc3VfpzpvcRHZ3QPw9yEMVvmtCekaMeOHnbr3WbxfrgEnQTv6haGP4cmv0Ojmihzsxw==} engines: {node: '>=18.0.0'} @@ -18374,61 +18016,6 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)': - dependencies: - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.1.21 - '@opentelemetry/semantic-conventions': 1.41.1 - '@standard-schema/spec': 1.1.0 - better-call: 1.3.5(zod@4.4.3) - jose: 6.2.3 - kysely: 0.29.2 - nanostores: 1.3.0 - zod: 4.4.3 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - - '@better-auth/drizzle-adapter@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0(supports-color@10.2.2))(kysely@0.29.2)(mysql2@3.22.6(@types/node@24.13.3))(pg@8.22.0)(sql.js@1.14.1))': - dependencies: - '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 - optionalDependencies: - drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0(supports-color@10.2.2))(kysely@0.29.2)(mysql2@3.22.6(@types/node@24.13.3))(pg@8.22.0)(sql.js@1.14.1) - - '@better-auth/kysely-adapter@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(kysely@0.29.2)': - dependencies: - '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 - optionalDependencies: - kysely: 0.29.2 - - '@better-auth/memory-adapter@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)': - dependencies: - '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 - - '@better-auth/mongo-adapter@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)': - dependencies: - '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 - - '@better-auth/prisma-adapter@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)': - dependencies: - '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 - - '@better-auth/telemetry@1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)': - dependencies: - '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.1.21 - - '@better-auth/utils@0.4.1': - dependencies: - '@noble/hashes': 2.0.1 - - '@better-fetch/fetch@1.1.21': {} - '@braintree/sanitize-url@7.1.2': {} '@bramus/specificity@2.4.2': @@ -20515,10 +20102,6 @@ snapshots: - undici - utf-8-validate - '@hono/node-server@1.19.14(hono@4.12.24)': - dependencies: - hono: 4.12.24 - '@iconify/types@2.0.0': {} '@iconify/utils@3.1.3': @@ -21371,49 +20954,14 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 - '@modelcontextprotocol/sdk@1.26.0(supports-color@10.2.2)(zod@4.4.3)': + '@modelcontextprotocol/core@2.0.0': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.24) - 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.1.0 - express: 5.2.1(supports-color@10.2.2) - express-rate-limit: 8.5.2(express@5.2.1(supports-color@10.2.2)) - hono: 4.12.24 - jose: 6.2.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) - transitivePeerDependencies: - - supports-color - '@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.4.3)': + '@modelcontextprotocol/server@2.0.0': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.24) - 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.1.0 - express: 5.2.1(supports-color@10.2.2) - express-rate-limit: 8.5.2(express@5.2.1(supports-color@10.2.2)) - hono: 4.12.24 - jose: 6.2.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 + '@modelcontextprotocol/core': 2.0.0 zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) - transitivePeerDependencies: - - supports-color '@module-federation/error-codes@0.22.0': {} @@ -21629,8 +21177,6 @@ snapshots: transitivePeerDependencies: - react-dom - '@noble/ciphers@2.2.0': {} - '@noble/hashes@1.4.0': {} '@noble/hashes@2.0.1': {} @@ -21817,8 +21363,6 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@opentelemetry/semantic-conventions@1.41.1': {} - '@oxc-project/types@0.138.0': {} '@oxfmt/binding-android-arm-eabi@0.58.0': @@ -23069,32 +22613,6 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@redis/bloom@1.2.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/client@1.6.1': - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - - '@redis/graph@1.1.1(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/json@1.0.7(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/search@1.2.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/time-series@1.1.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7)': dependencies: '@standard-schema/spec': 1.1.0 @@ -25243,11 +24761,6 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -25618,50 +25131,6 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0(supports-color@10.2.2))(kysely@0.29.2)(mysql2@3.22.6(@types/node@24.13.3))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.6(@types/node@24.13.3))(next@16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(solid-js@1.9.14)(vitest@4.1.10)(vue@3.5.35(typescript@7.0.2)): - dependencies: - '@better-auth/core': 1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0) - '@better-auth/drizzle-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0(supports-color@10.2.2))(kysely@0.29.2)(mysql2@3.22.6(@types/node@24.13.3))(pg@8.22.0)(sql.js@1.14.1)) - '@better-auth/kysely-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(kysely@0.29.2) - '@better-auth/memory-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1) - '@better-auth/mongo-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1) - '@better-auth/prisma-adapter': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1) - '@better-auth/telemetry': 1.6.15(@better-auth/core@1.6.15(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.1)(@better-fetch/fetch@1.1.21) - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.1.21 - '@noble/ciphers': 2.2.0 - '@noble/hashes': 2.0.1 - better-call: 1.3.5(zod@4.4.3) - defu: 6.1.7 - jose: 6.2.3 - kysely: 0.29.2 - nanostores: 1.3.0 - zod: 4.4.3 - optionalDependencies: - better-sqlite3: 12.11.1 - drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0(supports-color@10.2.2))(kysely@0.29.2)(mysql2@3.22.6(@types/node@24.13.3))(pg@8.22.0)(sql.js@1.14.1) - mysql2: 3.22.6(@types/node@24.13.3) - next: 16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) - pg: 8.22.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - solid-js: 1.9.14 - vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) - vue: 3.5.35(typescript@7.0.2) - transitivePeerDependencies: - - '@cloudflare/workers-types' - - '@opentelemetry/api' - - better-call@1.3.5(zod@4.4.3): - dependencies: - '@better-auth/utils': 0.4.1 - '@better-fetch/fetch': 1.1.21 - rou3: 0.7.12 - set-cookie-parser: 3.1.0 - optionalDependencies: - zod: 4.4.3 - better-sqlite3@12.11.1: dependencies: bindings: 1.5.0 @@ -25706,20 +25175,6 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.2(supports-color@10.2.2): - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3(supports-color@10.2.2) - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.15.2 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - bonjour-service@1.4.0: dependencies: fast-deep-equal: 3.1.3 @@ -26006,8 +25461,6 @@ snapshots: cluster-key-slot@1.1.1: {} - cluster-key-slot@1.1.2: {} - cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) @@ -26081,8 +25534,6 @@ snapshots: commander@10.0.1: {} - commander@11.1.0: {} - commander@2.20.3: {} commander@5.1.0: {} @@ -26154,12 +25605,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 - content-disposition@1.1.0: {} - content-type@1.0.5: {} - content-type@2.0.0: {} - conventional-changelog-angular@8.0.0: dependencies: compare-func: 2.0.0 @@ -26192,8 +25639,6 @@ snapshots: cookie-signature@1.0.7: {} - cookie-signature@1.2.2: {} - cookie@0.7.2: {} cookie@1.1.1: {} @@ -26229,11 +25674,6 @@ snapshots: core-util-is@1.0.3: {} - 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 @@ -27241,10 +26681,6 @@ snapshots: eventsource-parser@3.1.0: {} - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.1.0 - execa@10.0.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -27303,11 +26739,6 @@ snapshots: expect-type@1.3.0: {} - express-rate-limit@8.5.2(express@5.2.1(supports-color@10.2.2)): - dependencies: - express: 5.2.1(supports-color@10.2.2) - ip-address: 10.2.0 - express@4.22.2(supports-color@10.2.2): dependencies: accepts: 1.3.8 @@ -27344,39 +26775,6 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1(supports-color@10.2.2): - dependencies: - accepts: 2.0.0 - body-parser: 2.2.2(supports-color@10.2.2) - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@10.2.2) - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1(supports-color@10.2.2) - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.2 - range-parser: 1.2.1 - router: 2.2.0(supports-color@10.2.2) - send: 1.2.1(supports-color@10.2.2) - serve-static: 2.2.1(supports-color@10.2.2) - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -27535,17 +26933,6 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@2.1.1(supports-color@10.2.2): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - 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-cache-dir@4.0.0: dependencies: common-path-prefix: 3.0.0 @@ -27612,8 +26999,6 @@ snapshots: fresh@0.5.2: {} - fresh@2.0.0: {} - fs-constants@1.0.0: {} fs-extra@11.2.0: @@ -27652,8 +27037,6 @@ snapshots: dependencies: is-property: 1.0.2 - generic-pool@3.9.0: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -28052,8 +27435,6 @@ snapshots: dependencies: react-is: 19.2.7 - hono@4.12.24: {} - hook-std@4.0.0: {} hookable@6.1.1: {} @@ -28331,8 +27712,6 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.2.0: {} - ip-address@9.0.5: dependencies: jsbn: 1.1.0 @@ -28430,8 +27809,6 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-promise@4.0.0: {} - is-property@1.0.2: {} is-regexp@1.0.0: {} @@ -28626,8 +28003,6 @@ snapshots: json-schema-traverse@1.0.0: {} - json-schema-typed@8.0.2: {} - json-schema@0.4.0: {} json5@2.2.3: {} @@ -28679,7 +28054,8 @@ snapshots: kuler@2.0.0: {} - kysely@0.29.2: {} + kysely@0.29.2: + optional: true latest-version@7.0.0: dependencies: @@ -28935,24 +28311,6 @@ snapshots: math-intrinsics@1.1.0: {} - mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.26.0(supports-color@10.2.2)(zod@4.4.3))(next@16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)): - dependencies: - '@modelcontextprotocol/sdk': 1.26.0(supports-color@10.2.2)(zod@4.4.3) - chalk: 5.6.2 - commander: 11.1.0 - redis: 4.7.1 - optionalDependencies: - next: 16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) - - mcp-handler@1.1.0(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.4.3))(next@16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)): - dependencies: - '@modelcontextprotocol/sdk': 1.29.0(supports-color@10.2.2)(zod@4.4.3) - chalk: 5.6.2 - commander: 11.1.0 - redis: 4.7.1 - optionalDependencies: - next: 16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) - mdast-util-directive@3.1.0(supports-color@10.2.2): dependencies: '@types/mdast': 4.0.4 @@ -29160,8 +28518,6 @@ snapshots: media-typer@0.3.0: {} - media-typer@1.1.0: {} - medium-zoom@1.1.0: {} memfs@4.57.6(tslib@2.8.1): @@ -29185,8 +28541,6 @@ snapshots: merge-descriptors@1.0.3: {} - merge-descriptors@2.0.0: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -29670,8 +29024,6 @@ snapshots: nanoid@6.0.1: {} - nanostores@1.3.0: {} - napi-build-utils@1.0.2: {} negotiator@0.6.3: {} @@ -30149,8 +29501,6 @@ snapshots: path-to-regexp@6.3.0: optional: true - path-to-regexp@8.4.2: {} - path-type@4.0.0: {} pathe@2.0.3: {} @@ -31105,13 +30455,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -31428,15 +30771,6 @@ snapshots: dependencies: redis-errors: 1.2.0 - redis@4.7.1: - dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.6.1) - '@redis/client': 1.6.1 - '@redis/graph': 1.1.1(@redis/client@1.6.1) - '@redis/json': 1.0.7(@redis/client@1.6.1) - '@redis/search': 1.2.0(@redis/client@1.6.1) - '@redis/time-series': 1.1.0(@redis/client@1.6.1) - redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 @@ -31685,8 +31019,6 @@ snapshots: rope-sequence@1.3.4: {} - rou3@0.7.12: {} - roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -31694,16 +31026,6 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 - router@2.2.0(supports-color@10.2.2): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - rtlcss@4.3.0: dependencies: escalade: 3.2.0 @@ -31850,22 +31172,6 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.1(supports-color@10.2.2): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - 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 - serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -31907,15 +31213,6 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1(supports-color@10.2.2): - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - set-cookie-parser@2.7.2: {} set-cookie-parser@3.1.0: {} @@ -32604,28 +31901,6 @@ snapshots: trough@2.2.0: {} - trpc-to-mcp@1.3.2(09f4f932022b61bbb1db30a9d75546c7): - dependencies: - '@modelcontextprotocol/sdk': 1.29.0(supports-color@10.2.2)(zod@4.4.3) - '@trpc/server': 11.18.0(typescript@7.0.2) - better-auth: 1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0(supports-color@10.2.2))(kysely@0.29.2)(mysql2@3.22.6(@types/node@24.13.3))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.6(@types/node@24.13.3))(next@16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(solid-js@1.9.14)(vitest@4.1.10)(vue@3.5.35(typescript@7.0.2)) - mcp-handler: 1.1.0(@modelcontextprotocol/sdk@1.26.0(supports-color@10.2.2)(zod@4.4.3))(next@16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) - zod: 4.4.3 - transitivePeerDependencies: - - '@cfworker/json-schema' - - supports-color - - trpc-to-mcp@1.3.2(7c45d4095abf7cef4938a6fc2cc62863): - dependencies: - '@modelcontextprotocol/sdk': 1.29.0(supports-color@10.2.2)(zod@4.4.3) - '@trpc/server': 11.18.0(typescript@7.0.2) - better-auth: 1.6.15(@opentelemetry/api@1.9.0)(better-sqlite3@12.11.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.11.1)(gel@2.0.0(supports-color@10.2.2))(kysely@0.29.2)(mysql2@3.22.6(@types/node@24.13.3))(pg@8.22.0)(sql.js@1.14.1))(mysql2@3.22.6(@types/node@24.13.3))(next@16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(pg@8.22.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(solid-js@1.9.14)(vitest@4.1.10)(vue@3.5.35(typescript@7.0.2)) - mcp-handler: 1.1.0(@modelcontextprotocol/sdk@1.29.0(supports-color@10.2.2)(zod@4.4.3))(next@16.3.0-preview.9(@babel/core@7.29.0(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.61.1)(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)) - zod: 4.4.3 - transitivePeerDependencies: - - '@cfworker/json-schema' - - supports-color - trpc-to-openapi@3.3.0(patch_hash=2ca3c16af0fcca0c736697ad4fe553a14f794524fa9ce0d5c3e8ee4aea76090c)(@trpc/server@11.18.0(typescript@7.0.2))(zod-openapi@5.4.6(zod@4.4.3))(zod@4.4.3): dependencies: '@trpc/server': 11.18.0(typescript@7.0.2) @@ -32716,12 +31991,6 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - typedarray-to-buffer@3.1.5: dependencies: is-typedarray: 1.0.0 @@ -33473,8 +32742,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yaml@2.9.0: {} yargs-parser@20.2.9: {} @@ -33539,10 +32806,6 @@ snapshots: dependencies: zod: 4.4.3 - zod-to-json-schema@3.25.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - zod-validation-error@5.0.0(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 70cc23136..703b69218 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -34,7 +34,7 @@ catalog: "@immich/sdk": ^3.0.2 "@jellyfin/sdk": ^0.13.0 "@kubernetes/client-node": ^1.4.0 - "@modelcontextprotocol/sdk": 1.29.0 + "@modelcontextprotocol/server": 2.0.0 "@mantine/charts": ^9.4.1 "@mantine/colors-generator": ^9.4.1 "@mantine/core": ^9.4.1 @@ -165,7 +165,6 @@ catalog: mantine-datatable: ^9.3.1 mantine-react-table: npm:mantine-react-table-open@9.0.3 mermaid: ^11.15.0 - mcp-handler: ^1.1.0 maria2: ^0.4.1 mysql2: 3.22.6 next: 16.3.0-preview.9 @@ -197,7 +196,6 @@ catalog: svgson: ^5.3.1 testcontainers: ^12.0.4 timezones-ical-library: ^2.2.1 - trpc-to-mcp: ^1.3.2 trpc-to-openapi: ^3.3.0 tsdav: ^2.3.0 tsx: 4.20.4