feat: migrate MCP server to v2

This commit is contained in:
ajnart committed 2026-08-13 19:56:55 +02:00
1 parent aab12bbe64
commit e4cb8da5e2
17 files changed
+720 -469

No files matched your search

+83
View File
@@ -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<string, Record<string, unknown>>;
const required = ((schema.required as string[] | undefined) ?? []).filter(
(key) => !("default" in (properties[key] ?? {})),
);
return {
...schema,
type: "object",
properties,
required,
};
};
export function extractMcpToolsFromProcedures<TRoot extends AnyRootTypes, TRecord extends RouterRecord>(
router: Router<TRoot, TRecord>,
): 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));
}
+2
View File
@@ -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";
+1 -1
View File
@@ -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" });