172 lines
6.0 KiB
TypeScript
172 lines
6.0 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod/v4";
|
|
|
|
import { CUSTOM_WIDGET_MCP_AUTHORING_PROMPT } from "@homarr/custom-widgets/authoring-prompt";
|
|
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";
|
|
|
|
export const metadataProcedures = {
|
|
schema: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.meta({ mcp: { enabled: true, description: "Get the current Custom JSX widget JSON Schema." } })
|
|
.query(() => getCustomWidgetJsonSchema()),
|
|
|
|
getAuthoringPrompt: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.meta({ mcp: { enabled: true, description: "Get the current Custom Widget authoring instructions." } })
|
|
.query(() => ({
|
|
version: 2,
|
|
prompt: CUSTOM_WIDGET_MCP_AUTHORING_PROMPT,
|
|
resources: [
|
|
"homarr://custom-widgets/schema",
|
|
"homarr://custom-widgets/components",
|
|
"homarr://custom-widgets/skill",
|
|
],
|
|
httpResources: ["/api/custom-widgets/schema", "/api/custom-widgets/components", "/api/custom-widgets/skill"],
|
|
})),
|
|
|
|
getSkill: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.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 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 authoringResult = customWidgetAuthoringDefinitionSchema.safeParse(input.widget);
|
|
if (!authoringResult.success) {
|
|
return {
|
|
valid: false as const,
|
|
issues: authoringResult.error.issues.map((issue) => ({
|
|
path: issue.path.map(String).join("."),
|
|
code: issue.code,
|
|
message: issue.message,
|
|
})),
|
|
};
|
|
}
|
|
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;
|
|
}
|
|
}),
|
|
};
|