fix: harden assistant authoring and UI
This commit is contained in:
1 parent
fa0e55cc93
commit
edc7b4d55b
51 files changed
+4118
-916
No files matched your search
@@ -1,8 +1,19 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { CUSTOM_WIDGET_MCP_AUTHORING_PROMPT } from "@homarr/custom-widgets/authoring-prompt";
|
||||
import { getCustomWidgetSkill } from "@homarr/custom-widgets/authoring-resources";
|
||||
import { customWidgetImportSchema, getCustomWidgetJsonSchema } from "@homarr/custom-widgets/core";
|
||||
import {
|
||||
getCustomWidgetComponent,
|
||||
getCustomWidgetComponentCatalog,
|
||||
getCustomWidgetExample,
|
||||
getCustomWidgetSharedProps,
|
||||
getCustomWidgetSkill,
|
||||
} from "@homarr/custom-widgets/authoring-resources";
|
||||
import {
|
||||
customWidgetAuthoringDefinitionSchema,
|
||||
getCustomWidgetJsonSchema,
|
||||
normalizeCustomWidgetAuthoringDefinition,
|
||||
} from "@homarr/custom-widgets/core";
|
||||
|
||||
import { permissionRequiredProcedure } from "../../trpc";
|
||||
|
||||
@@ -28,25 +39,133 @@ export const metadataProcedures = {
|
||||
|
||||
getSkill: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.meta({ mcp: { enabled: true, description: "Get the portable Homarr Custom Widget skill." } })
|
||||
.meta({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"Load the complete Homarr Custom Widget authoring skill in one response, including SKILL.md and every bundled reference. Call this before authoring, repairing, validating, previewing, or creating a custom widget.",
|
||||
},
|
||||
})
|
||||
.query(() => getCustomWidgetSkill()),
|
||||
|
||||
getComponentCatalog: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.meta({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"List a compact index of supported Custom JSX component names, categories, shared prop names, safety policies, and example IDs. Fetch only the named details needed for the widget.",
|
||||
},
|
||||
})
|
||||
.query(() => getCustomWidgetComponentCatalog()),
|
||||
|
||||
getComponent: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.meta({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"Get the installed Custom JSX documentation, allowed props, blocked props, and accessibility requirements for one named component.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ name: z.string().trim().min(1).max(128).describe("A component name from the catalog.") }))
|
||||
.query(({ input }) => {
|
||||
const component = getCustomWidgetComponent(input.name);
|
||||
if (!component) throw new TRPCError({ code: "NOT_FOUND", message: "Custom JSX component not found" });
|
||||
return component;
|
||||
}),
|
||||
|
||||
getSharedProps: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.meta({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"Get documentation for only the named shared Custom JSX props. Collect the shared prop names needed by all selected components and fetch them together once.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
names: z
|
||||
.array(z.string().trim().min(1).max(128))
|
||||
.min(1)
|
||||
.max(64)
|
||||
.describe("Shared prop names from customWidget_getComponentCatalog.sharedProps.names."),
|
||||
}),
|
||||
)
|
||||
.query(({ input }) => getCustomWidgetSharedProps(input.names)),
|
||||
|
||||
getExample: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.meta({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"Get one installed Custom JSX example by its catalog ID. Fetch only an example relevant to the requested interaction pattern.",
|
||||
},
|
||||
})
|
||||
.input(z.object({ name: z.string().trim().min(1).max(128).describe("An example ID from the catalog.") }))
|
||||
.query(({ input }) => {
|
||||
const example = getCustomWidgetExample(input.name);
|
||||
if (!example) throw new TRPCError({ code: "NOT_FOUND", message: "Custom JSX example not found" });
|
||||
return example;
|
||||
}),
|
||||
|
||||
validate: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.meta({ mcp: { enabled: true, description: "Validate one Custom JSX widget without saving it." } })
|
||||
.input(z.object({ widget: z.unknown() }))
|
||||
.meta({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"Validate one complete Custom JSX widget without saving it. Multiline JSX may be supplied as templateLines; the result returns precise issue paths to repair before previewing.",
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
widget: z
|
||||
.unknown()
|
||||
.describe("A complete Homarr Custom Widget v2 definition using either template or templateLines."),
|
||||
}),
|
||||
)
|
||||
.query(({ input }) => {
|
||||
const result = customWidgetImportSchema.safeParse(input.widget);
|
||||
if (!result.success) {
|
||||
const authoringResult = customWidgetAuthoringDefinitionSchema.safeParse(input.widget);
|
||||
if (!authoringResult.success) {
|
||||
return {
|
||||
valid: false as const,
|
||||
issues: result.error.issues.map((issue) => ({
|
||||
issues: authoringResult.error.issues.map((issue) => ({
|
||||
path: issue.path.map(String).join("."),
|
||||
code: issue.code,
|
||||
message: issue.message,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return { valid: true as const, issues: [], widget: result.data };
|
||||
try {
|
||||
const widget = normalizeCustomWidgetAuthoringDefinition(authoringResult.data);
|
||||
return {
|
||||
valid: true as const,
|
||||
issues: [],
|
||||
summary: {
|
||||
name: widget.name,
|
||||
sourceIds: Object.keys(widget.sources),
|
||||
requestIds: Object.keys(widget.requests),
|
||||
optionIds: Object.keys(widget.options),
|
||||
templateLineCount: widget.template.split("\n").length,
|
||||
},
|
||||
nextStep:
|
||||
"Reuse the validated authoring definition with customWidget_previewCreate, then run every returned query.",
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
valid: false as const,
|
||||
issues: error.issues.map((issue) => ({
|
||||
path: issue.path.map(String).join("."),
|
||||
code: issue.code,
|
||||
message: issue.message,
|
||||
})),
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
};
|
||||
Reference in new issue
Block a user