diff --git a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.spec.ts b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.spec.ts index a2bd5be4..d454dad5 100644 --- a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.spec.ts +++ b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.spec.ts @@ -24,6 +24,14 @@ import { SHARED_TOOL_SPECS } from '../../../../../../packages/mcp/src/tool-specs const mockLoaded = (DocmostClient: loader.DocmostClientCtor) => ({ DocmostClient, sharedToolSpecs: SHARED_TOOL_SPECS as Record, + // Pure no-network draw.io helpers (#424). Type-correct stubs: these tests + // never execute the drawio_shapes / drawio_guide tool bodies. + searchShapes: (() => []) as unknown as loader.SearchShapesFn, + getGuideSection: (() => ({ + section: 'index', + content: '', + sections: [], + })) as unknown as loader.GetGuideSectionFn, }); /** diff --git a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts index c40bdbfd..bc22515c 100644 --- a/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts +++ b/apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts @@ -111,10 +111,12 @@ function __assertClientCallContract(client: DocmostClientLike): void { afterText: s, }); void client.replaceImage(s, s, s, { align, alt: s }); - // --- draw.io diagrams (#423) --- + // --- draw.io diagrams (#423 stage 1, #424 stage 2) --- + // The 5th `layout` arg (#424) is exercised so this parity assertion fails if the + // client signature drops it — it must reach the client from the shared execute. void client.drawioGet(s, s, 'xml'); - void client.drawioCreate(s, { position: 'append', anchorNodeId: s }, s, s); - void client.drawioUpdate(s, s, s, s); + void client.drawioCreate(s, { position: 'append', anchorNodeId: s }, s, s, 'elk'); + void client.drawioUpdate(s, s, s, s, 'elk'); // --- write (comment) --- void client.createComment(s, s, 'inline', s, s, s); void client.resolveComment(s, true); @@ -263,6 +265,11 @@ export class AiChatToolsService { // provenance tokens) and load the shared tool-spec registry. Client // construction is shared with the page-change detection path (#274) via // buildDocmostClient so both go over the exact same authenticated route. + // drawio_shapes / drawio_guide (#424) are NOT destructured here anymore: they + // are ordinary SHARED_TOOL_SPECS entries whose canonical execute (in the mcp + // package) calls the pure searchShapes / getGuideSection helpers directly, so + // the registry loop below wires them for the in-app host too — no hand-mirrored + // handler and no direct helper import in this service. const { sharedToolSpecs, createCommentSignalTracker } = await loadDocmostMcp(); const client = await this.buildDocmostClient( diff --git a/apps/server/src/core/ai-chat/tools/docmost-client.loader.ts b/apps/server/src/core/ai-chat/tools/docmost-client.loader.ts index 4573199b..ecd0076b 100644 --- a/apps/server/src/core/ai-chat/tools/docmost-client.loader.ts +++ b/apps/server/src/core/ai-chat/tools/docmost-client.loader.ts @@ -141,6 +141,19 @@ export type CommentSignalTrackerFactory = (options: { debounceMs?: number; }) => CommentSignalTrackerLike; +// Pure, no-network draw.io helpers (#424). These are plain functions on the +// module (NOT DocmostClient methods) — the in-app AI-SDK service calls them +// directly to wire drawio_shapes / drawio_guide, mirroring the MCP server. +export type SearchShapesFn = ( + query: string, + opts?: { category?: string; limit?: number }, +) => Array>; +export type GetGuideSectionFn = (section?: string) => { + section: string; + content: string; + sections: string[]; +}; + interface DocmostMcpModule { DocmostClient: DocmostClientCtor; SHARED_TOOL_SPECS: Record; @@ -153,6 +166,14 @@ interface DocmostMcpModule { // the mocked loader in unit tests) — the stale-check below is a NO-OP when it // is missing, so an older build never wrongly fails startup. REGISTRY_STAMP?: string; + // Pure, no-network draw.io helpers (#424). Still exposed off the loaded module + // so unit-test loader mocks can stub them, but the in-app tool wiring no longer + // calls them directly: drawio_shapes / drawio_guide are ordinary + // SHARED_TOOL_SPECS entries whose canonical execute (in the mcp package) invokes + // searchShapes / getGuideSection, so parity with the MCP host comes from the + // shared registry loop, not a hand-mirrored in-app handler. + searchShapes: SearchShapesFn; + getGuideSection: GetGuideSectionFn; } /** @@ -216,6 +237,8 @@ export async function loadDocmostMcp(): Promise<{ DocmostClient: DocmostClientCtor; sharedToolSpecs: Record; createCommentSignalTracker?: CommentSignalTrackerFactory; + searchShapes: SearchShapesFn; + getGuideSection: GetGuideSectionFn; }> { if (!modulePromise) { modulePromise = (async () => { @@ -261,5 +284,8 @@ export async function loadDocmostMcp(): Promise<{ // Optional: forwarded when present so the in-app layer can build the passive // comment signal (#417); undefined on a stale build => signal disabled. createCommentSignalTracker: mod.createCommentSignalTracker, + // Pure no-network draw.io helpers (#424); not client methods. + searchShapes: mod.searchShapes, + getGuideSection: mod.getGuideSection, }; } diff --git a/apps/server/src/core/ai-chat/tools/shared-tool-specs.contract.spec.ts b/apps/server/src/core/ai-chat/tools/shared-tool-specs.contract.spec.ts index f62f0cc7..3d708db0 100644 --- a/apps/server/src/core/ai-chat/tools/shared-tool-specs.contract.spec.ts +++ b/apps/server/src/core/ai-chat/tools/shared-tool-specs.contract.spec.ts @@ -45,6 +45,16 @@ describe('SHARED_TOOL_SPECS contract parity', () => { string, loader.SharedToolSpec >, + // Pure no-network draw.io helpers (#424). The contract test never executes + // a tool body, so type-correct stubs suffice (the real functions can't be + // imported here — drawio-shapes.ts uses import.meta, incompatible with the + // CommonJS jest transform). + searchShapes: (() => []) as unknown as loader.SearchShapesFn, + getGuideSection: (() => ({ + section: 'index', + content: '', + sections: [], + })) as unknown as loader.GetGuideSectionFn, }); const service = new AiChatToolsService( tokenServiceStub as never, diff --git a/apps/server/src/core/ai-chat/tools/tool-tiers.spec.ts b/apps/server/src/core/ai-chat/tools/tool-tiers.spec.ts index 130e9b06..3849af12 100644 --- a/apps/server/src/core/ai-chat/tools/tool-tiers.spec.ts +++ b/apps/server/src/core/ai-chat/tools/tool-tiers.spec.ts @@ -124,6 +124,13 @@ describe('deferred catalog ↔ live forUser() toolset partition (#332, F3)', () return {} as DocmostClientLike; } as unknown as loader.DocmostClientCtor, sharedToolSpecs: SHARED_TOOL_SPECS as Record, + // Pure no-network draw.io helpers (#424); tool bodies are never executed here. + searchShapes: (() => []) as unknown as loader.SearchShapesFn, + getGuideSection: (() => ({ + section: 'index', + content: '', + sections: [], + })) as unknown as loader.GetGuideSectionFn, }); const service = new AiChatToolsService( { diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index dbf425e5..12cb60d7 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -55,6 +55,13 @@ export type { CommentSignalProbeResult, CommentSignalTrackerOptions, } from "./comment-signal.js"; +// Re-export the pure, no-network draw.io helpers (#424) so the in-app AI-SDK +// service can wire drawio_shapes / drawio_guide off the loaded module. These are +// NOT client methods (no page/backend hit) — the in-app handler calls them +// directly, mirroring how the standalone MCP server wires them here. +export { searchShapes } from "./lib/drawio-shapes.js"; +export type { SearchShapesOptions } from "./lib/drawio-shapes.js"; +export { getGuideSection } from "./lib/drawio-guide.js"; // Read version from package.json const __filename = fileURLToPath(import.meta.url);