diff --git a/Dockerfile b/Dockerfile index c21f470..03b4817 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index be7a637..9645061 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/src/mcp/server.test.ts b/src/mcp/server.test.ts index 3a2d884..d0585c1 100644 --- a/src/mcp/server.test.ts +++ b/src/mcp/server.test.ts @@ -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();