From fa9d080eedb7b358bde9ffa9bf61b9e0df6c921f Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 9 Aug 2026 08:25:17 +0200 Subject: [PATCH] feat: migrate MCP server to v2 --- apps/docs/docs/management/mcp.mdx | 32 +- apps/nextjs/package.json | 4 +- .../oauth-authorization-server/route.ts | 1 + .../oauth-protected-resource/route.ts | 2 +- .../tools/api/components/mcp-instructions.tsx | 4 +- .../src/app/api/mcp/[transport]/route.spec.ts | 11 +- .../src/app/api/mcp/[transport]/route.ts | 444 +--------- apps/nextjs/src/app/api/mcp/_extract-tools.ts | 18 +- apps/nextjs/src/app/api/mcp/_handler.ts | 360 ++++++++ apps/nextjs/src/app/api/mcp/_protocol.spec.ts | 140 ++++ apps/nextjs/src/app/api/mcp/_protocol.ts | 89 ++ apps/nextjs/src/app/api/mcp/oauth/_store.ts | 4 + .../src/app/api/mcp/oauth/authorize/route.ts | 17 +- .../src/app/api/mcp/oauth/token/route.ts | 7 +- apps/nextjs/src/app/api/mcp/route.ts | 1 + packages/api/package.json | 1 - packages/api/src/mcp-tools.ts | 83 ++ packages/api/src/mcp.ts | 2 + packages/api/src/test/mcp.spec.ts | 17 +- packages/api/src/trpc.ts | 2 +- pnpm-lock.yaml | 769 +----------------- pnpm-workspace.yaml | 4 +- 22 files changed, 774 insertions(+), 1238 deletions(-) create mode 100644 apps/nextjs/src/app/api/mcp/_handler.ts create mode 100644 apps/nextjs/src/app/api/mcp/_protocol.spec.ts create mode 100644 apps/nextjs/src/app/api/mcp/_protocol.ts create mode 100644 apps/nextjs/src/app/api/mcp/route.ts create mode 100644 packages/api/src/mcp-tools.ts 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/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/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/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