feat: add optional styles guide resource

This commit is contained in:
2026-05-31 14:36:51 +00:00
parent 25bf171f6b
commit b4a540214b
3 changed files with 173 additions and 8 deletions
+79 -2
View File
@@ -1,4 +1,6 @@
import { applyPatch, type Operation } from "fast-json-patch";
import { accessSync, constants, readFileSync, statSync } from "node:fs";
import { extname, isAbsolute } from "node:path";
import { createMcpHandler } from "mcp-handler";
import { z } from "zod";
import { normalizeScene, normalizeTitle } from "../core/scene";
@@ -35,6 +37,12 @@ type ToolResult = {
content: Array<{ type: "text"; text: string }>;
};
type StylesGuideResource = {
path: string;
};
export const DEFAULT_STYLES_GUIDE_PATH = "/config/styles-guide.md";
function trimTrailingSlash(url: string): string {
return url.replace(/\/+$/, "");
}
@@ -56,6 +64,50 @@ export function resolvePublicBaseUrl(raw = process.env.PUBLIC_BASE_URL): string
throw new Error("PUBLIC_BASE_URL must be an absolute http(s) URL");
}
export function resolveStylesGuidePath(raw = DEFAULT_STYLES_GUIDE_PATH): StylesGuideResource | undefined {
const value = raw.trim();
if (!isAbsolute(value)) {
throw new Error("Styles guide path must be an absolute filesystem path");
}
let stats;
try {
stats = statSync(value);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
return undefined;
}
throw new Error(
error instanceof Error
? `Styles guide path could not be checked: ${error.message}`
: "Styles guide path could not be checked",
);
}
const extension = extname(value).toLowerCase();
if (extension !== ".md" && extension !== ".markdown") {
throw new Error("Styles guide path must point to a Markdown file (.md or .markdown)");
}
if (!stats.isFile()) {
throw new Error("Styles guide path must point to a regular file");
}
try {
accessSync(value, constants.R_OK);
} catch (error) {
throw new Error(
error instanceof Error
? `Styles guide path must point to a readable file: ${error.message}`
: "Styles guide path must point to a readable file",
);
}
return { path: value };
}
function drawingUrl(baseUrl: string, id: string): string {
return `${trimTrailingSlash(baseUrl)}/d/${id}`;
}
@@ -157,11 +209,36 @@ export function createMcpToolHandlers(store: DrawingStore, publicBaseUrl: string
};
}
export function createExcaliMcpHandler(store: DrawingStore, publicBaseUrl: string) {
export function createExcaliMcpHandler(
store: DrawingStore,
publicBaseUrl: string,
stylesGuide?: StylesGuideResource,
) {
const tools = createMcpToolHandlers(store, publicBaseUrl);
return createMcpHandler(
(server) => {
if (stylesGuide) {
server.registerResource(
"styles-guide",
"excali://styles-guide",
{
title: "Styles Guide",
description: "User-provided drawing styles guide for scene generation",
mimeType: "text/markdown",
},
async (uri) => ({
contents: [
{
uri: uri.toString(),
mimeType: "text/markdown",
text: readFileSync(stylesGuide.path, "utf8"),
},
],
}),
);
}
server.registerTool(
"list_drawings",
{
@@ -227,7 +304,7 @@ export function createExcaliMcpHandler(store: DrawingStore, publicBaseUrl: strin
export function createMcpServer() {
const store = createDrawingStore(process.env.DATABASE_PATH);
const handler = createExcaliMcpHandler(store, resolvePublicBaseUrl());
const handler = createExcaliMcpHandler(store, resolvePublicBaseUrl(), resolveStylesGuidePath());
return {
port: Number(process.env.MCP_PORT ?? "3001"),