feat: bundle default styles guide

This commit is contained in:
2026-05-31 19:14:02 +00:00
parent b60b921ff8
commit 4cbb55b81c
3 changed files with 12 additions and 81 deletions
+4 -1
View File
@@ -19,14 +19,17 @@ ENV MCP_HOST=127.0.0.1
ENV MCP_PORT=3001
ENV DATABASE_PATH=/data/excalidraw.sqlite
RUN mkdir -p /config /data
COPY --from=caddy /usr/bin/caddy /usr/bin/caddy
COPY --from=build /app/dist/public /srv/public
COPY --from=build /app/dist/server /app/dist/server
COPY --from=build /app/dist/mcp /app/dist/mcp
COPY Caddyfile /etc/caddy/Caddyfile
COPY STYLES_GUIDE.md /config/styles-guide.md
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN mkdir -p /data && chmod +x /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
VOLUME ["/data"]
+4 -4
View File
@@ -16,7 +16,6 @@ docker run --rm \
-p 127.0.0.1:3000:80 \
-e PUBLIC_BASE_URL=http://localhost:3000 \
-v "$PWD/excali-box-data:/data" \
-v "$PWD/STYLES_GUIDE.md:/config/styles-guide.md:ro" \
excali
```
@@ -27,7 +26,6 @@ docker run --rm \
-p 127.0.0.1:3000:80 \
-e PUBLIC_BASE_URL=http://localhost:3000 \
-v "$PWD/excali-box-data:/data" \
-v "$PWD/STYLES_GUIDE.md:/config/styles-guide.md:ro" \
ghcr.io/ruinivist/excalidraw-box:latest
```
@@ -70,7 +68,8 @@ This will make the data persistent in the `excali-box-data` folder in the curren
## Optional MCP styles guide resource
The MCP server can optionally expose one extra read-only resource at `excali://styles-guide`. This file is then
discoverable as an MCP resource.
discoverable as an MCP resource. Container images already include the repo `STYLES_GUIDE.md` at
`/config/styles-guide.md`, so the default guide is exposed automatically in Docker/GHCR.
```bash
docker run --rm \
@@ -81,4 +80,5 @@ docker run --rm \
ghcr.io/ruinivist/excalidraw-box:latest
```
Repo-root `STYLES_GUIDE.md` which is what I wrote for myself is not used at all unless you explicitly mount it to `/config/styles-guide.md`.
Bind-mounting a different file to `/config/styles-guide.md` overrides the bundled default for that container.
Local non-container runs are unchanged; if you want a styles guide there, you still need a file at `/config/styles-guide.md`.
+4 -76
View File
@@ -1,26 +1,17 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { afterEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { join } from "node:path";
import { type ScenePayload } from "../core/shared";
import { createDrawingStore, type DrawingStore } from "../server/db";
import {
DEFAULT_STYLES_GUIDE_PATH,
createExcaliMcpHandler,
createMcpToolHandlers,
resolvePublicBaseUrl,
resolveStylesGuidePath,
} from "./server";
import { createMcpToolHandlers, resolvePublicBaseUrl } from "./server";
const cleanup: Array<{ dir: string; store?: DrawingStore; client?: Client }> = [];
const cleanup: Array<{ dir: string; store?: DrawingStore }> = [];
afterEach(async () => {
while (cleanup.length > 0) {
const item = cleanup.pop();
if (item) {
await item.client?.close().catch(() => undefined);
item.store?.close();
rmSync(item.dir, { recursive: true, force: true });
}
@@ -37,35 +28,6 @@ function createTempTools() {
};
}
function createTempStylesGuidePath(content?: string) {
const dir = mkdtempSync(join(tmpdir(), "excali-mcp-"));
const path = join(dir, DEFAULT_STYLES_GUIDE_PATH.slice(1));
mkdirSync(dirname(path), { recursive: true });
if (content !== undefined) {
writeFileSync(path, content);
}
cleanup.push({ dir });
return path;
}
async function createTempClient(stylesGuidePath?: string) {
const dir = mkdtempSync(join(tmpdir(), "excali-mcp-"));
const store = createDrawingStore(join(dir, "test.sqlite"));
const handler = createExcaliMcpHandler(
store,
"http://example.test",
stylesGuidePath ? resolveStylesGuidePath(stylesGuidePath) : undefined,
);
const transport = new StreamableHTTPClientTransport(new URL("http://localhost/mcp"), {
fetch: async (input, init) => handler(new Request(input, init)),
});
const client = new Client({ name: "excali-test", version: "1.0.0" });
await client.connect(transport);
cleanup.push({ dir, store, client });
return { client, store };
}
function testScene(): ScenePayload {
return {
elements: [
@@ -82,40 +44,6 @@ describe("mcp tool handlers", () => {
expect(() => resolvePublicBaseUrl("")).toThrow("PUBLIC_BASE_URL is required");
});
test("missing fixed styles-guide path exposes no styles-guide resource", async () => {
const stylesGuidePath = createTempStylesGuidePath();
expect(resolveStylesGuidePath(stylesGuidePath)).toBeUndefined();
const { client } = await createTempClient(stylesGuidePath);
expect(client.getServerCapabilities()?.resources).toBeUndefined();
});
test("valid fixed styles-guide path registers the styles-guide resource", async () => {
const contents = "# Scene style\n\nKeep labels terse.\n";
const stylesGuidePath = createTempStylesGuidePath(contents);
const resolved = resolveStylesGuidePath(stylesGuidePath);
const { client } = await createTempClient(stylesGuidePath);
const resources = await client.listResources();
const readResult = await client.readResource({ uri: "excali://styles-guide" });
expect(resolved).toEqual({ path: stylesGuidePath });
expect(resources.resources).toContainEqual({
name: "styles-guide",
uri: "excali://styles-guide",
title: "Styles Guide",
description: "User-provided drawing styles guide for scene generation",
mimeType: "text/markdown",
});
expect(readResult.contents).toEqual([
{
uri: "excali://styles-guide",
mimeType: "text/markdown",
text: contents,
},
]);
});
test("create_drawing writes an explicit scene to a temp SQLite DB", () => {
const { store, tools } = createTempTools();
const scene = testScene();