feat(assistant): optimize MCP and custom widget authoring (#6743)

* feat(assistant): optimize MCP and custom widget authoring

* fix(assistant): isolate server-safe JSX analyzer

* refactor(assistant): generate MCP tool staging

* fix(assistant): preserve widget placement follow-up

* fix(ci): allow fast gate to finish

* fix(custom-widgets): address review findings

* fix(custom-widgets): split preview evidence module

* style: format assistant custom widget files

* fix(custom-widgets): sync formatted skill schema
This commit is contained in:
Thomas authored and GitHub committed 2026-08-30 23:43:29 +02:00
1 parent 38f4b0e1f8
commit 5e23659d99
95 files changed
+6799 -1778

No files matched your search

+19 -44
View File
@@ -12,10 +12,12 @@ import { ipAddressFromHeaders } from "@homarr/common/server";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { buildCustomWidgetMcpPrompt } from "@homarr/custom-widgets/authoring-prompt";
import {
CUSTOM_WIDGET_SKILL_REFERENCE_NAMES,
getCustomWidgetComponent,
getCustomWidgetComponentCatalog,
getCustomWidgetExample,
getCustomWidgetSkillContent,
getCustomWidgetSkillEntrypoint,
getCustomWidgetSkillReference,
} from "@homarr/custom-widgets/authoring-resources";
import { customJsxExamples, getCustomWidgetJsonSchema } from "@homarr/custom-widgets/core";
import { db } from "@homarr/db";
@@ -89,7 +91,7 @@ function registerCustomWidgetAuthoring(server: McpServer) {
server.registerPrompt(
"homarr-custom-widget-author",
{
description: "Author and iterate on one Homarr Custom JSX v2 widget.",
description: "Author and iterate on one or more Homarr Custom JSX v2 widgets.",
argsSchema: z.object({
request: z.string().optional().describe("The widget the user wants."),
documentationUrl: z.string().optional().describe("External API documentation URL."),
@@ -138,10 +140,21 @@ function registerCustomWidgetAuthoring(server: McpServer) {
"homarr://custom-widgets/skill",
{ mimeType: "text/markdown" },
(uri) => ({
contents: [{ uri: uri.href, mimeType: "text/markdown", text: getCustomWidgetSkillContent() }],
contents: [{ uri: uri.href, mimeType: "text/markdown", text: getCustomWidgetSkillEntrypoint().skillMd }],
}),
);
for (const name of CUSTOM_WIDGET_SKILL_REFERENCE_NAMES) {
server.registerResource(
`Custom Widget reference: ${name}`,
`homarr://custom-widgets/references/${name}`,
{ mimeType: "text/markdown" },
(uri) => ({
contents: [{ uri: uri.href, mimeType: "text/markdown", text: getCustomWidgetSkillReference(name).content }],
}),
);
}
for (const example of customJsxExamples) {
server.registerResource(
`Custom Widget example: ${example.title}`,
@@ -210,49 +223,11 @@ function sanitizeErrorMessage(error: unknown, toolName: string): string {
return "Tool execution failed";
}
const SERVER_INSTRUCTIONS = `You are connected to Homarr, a self-hosted homelab dashboard.
const SERVER_INSTRUCTIONS = `You are connected to Homarr, a self-hosted homelab dashboard. Use the listed tools and their descriptions for live data and actions; never invent resources, IDs, state, or results.
## Key Concepts
Integrations connect Homarr to services, boards contain widgets, and apps are visual links. Discover an integration before passing its integrationId to another tool. Returned permissions are authoritative: use access permits reads, interact access permits service actions, and full access permits configuration changes. Explain denied or unavailable access without suggesting a bypass.
**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 \`targets\` containing the endpointId and id returned for each container.
**"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.`;
Read current state when it matters, reuse results across a batch, and complete the requested work before summarizing. Load optional resources only when the task needs them. For Custom JSX authoring, start with the compact Custom Widget skill resource or prompt, then fetch only the live schema and named references or component documentation required by the design.`;
function extractApiKeyValue(req: NextRequest): string | null {
const apiKeyHeader = req.headers.get(API_KEY_HEADER_NAME);