Skip to content

Commit 2af5eeb

Browse files
Merge pull request #43 from Mermaid-Chart/AT-360-new-mcp-tool-for-the-chat-with-mermaid
AT-360 Added Diagram Chat Functionality To Mermaid/sdk So MCP Server Can Directly Access Mermaid AI
2 parents a0a6d13 + 8341ab1 commit 2af5eeb

6 files changed

Lines changed: 158 additions & 21 deletions

File tree

.changeset/upset-owls-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mermaidchart/sdk': patch
3+
---
4+
5+
Add diagramChat method to SDK for external clients like MCP to interact with Mermaid chat

packages/sdk/src/index.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ describe('deleteDocument', () => {
180180
expect(deletedDoc.projectID).toStrictEqual(newDocument.projectID);
181181

182182
expect(await client.getDocuments(testProjectId)).not.toContainEqual(newDocument);
183-
});
183+
}, 20000); // 20 seconds — test makes 4 sequential HTTP calls
184184
});
185185

186186
describe('getDocument', () => {

packages/sdk/src/index.test.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,51 @@ describe('MermaidChart', () => {
8787
});
8888
});
8989

90+
describe('#diagramChat', () => {
91+
beforeEach(async () => {
92+
await client.setAccessToken('test-access-token');
93+
});
94+
95+
it('should parse JSON response with text and documentChatThreadID', async () => {
96+
const jsonResponse = {
97+
text: 'Hello, here is your diagram!',
98+
documentChatThreadID: 'thread-abc-123',
99+
documentID: 'doc-123',
100+
};
101+
102+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
103+
vi.spyOn((client as any).axios, 'post').mockResolvedValue({ data: jsonResponse });
104+
105+
const result = await client.diagramChat({
106+
message: 'Create a flowchart',
107+
documentID: 'doc-123',
108+
});
109+
110+
expect(result.text).toBe('Hello, here is your diagram!');
111+
expect(result.documentChatThreadID).toBe('thread-abc-123');
112+
expect(result.documentID).toBe('doc-123');
113+
});
114+
115+
it('should throw AICreditsLimitExceededError on 402', async () => {
116+
// Mock the underlying axios call to simulate a 402 response from the API
117+
// so the actual error-mapping logic inside diagramChat() is exercised.
118+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
119+
vi.spyOn((client as any).axios, 'post').mockRejectedValue({
120+
response: {
121+
status: 402,
122+
data: 'AI credits limit exceeded',
123+
},
124+
});
125+
126+
await expect(
127+
client.diagramChat({
128+
message: 'Create a flowchart',
129+
documentID: 'doc-123',
130+
}),
131+
).rejects.toThrow(AICreditsLimitExceededError);
132+
});
133+
});
134+
90135
describe('#repairDiagram', () => {
91136
beforeEach(async () => {
92137
await client.setAccessToken('test-access-token');
@@ -109,9 +154,14 @@ describe('MermaidChart', () => {
109154
});
110155

111156
it('should throw AICreditsLimitExceededError on 402', async () => {
112-
vi.spyOn(client, 'repairDiagram').mockRejectedValue(
113-
new AICreditsLimitExceededError('AI credits limit exceeded'),
114-
);
157+
// Mock the underlying axios call to simulate a 402 response from the API
158+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
159+
vi.spyOn((client as any).axios, 'post').mockRejectedValue({
160+
response: {
161+
status: 402,
162+
data: 'AI credits limit exceeded',
163+
},
164+
});
115165

116166
await expect(
117167
client.repairDiagram({

packages/sdk/src/index.ts

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import type {
1111
AuthState,
1212
AuthorizationData,
13+
DiagramChatRequest,
14+
DiagramChatResponse,
1315
Document,
1416
InitParams,
1517
MCDocument,
@@ -24,6 +26,30 @@ import { URLS } from './urls.js';
2426
const defaultBaseURL = 'https://www.mermaid.ai'; // "http://127.0.0.1:5174"
2527
const authorizationURLTimeout = 60_000;
2628

29+
/**
30+
* Re-throws an Axios error as {@link AICreditsLimitExceededError} when the
31+
* server responds with HTTP 402, otherwise rethrows the original error.
32+
*/
33+
function throwIfAICreditsExceeded(error: unknown): never {
34+
if (
35+
error &&
36+
typeof error === 'object' &&
37+
'response' in error &&
38+
error.response &&
39+
typeof error.response === 'object' &&
40+
'status' in error.response &&
41+
(error as { response: { status: number } }).response.status === 402
42+
) {
43+
const axiosError = error as { response: { status: number; data?: unknown } };
44+
throw new AICreditsLimitExceededError(
45+
typeof axiosError.response.data === 'string'
46+
? axiosError.response.data
47+
: 'AI credits limit exceeded',
48+
);
49+
}
50+
throw error as Error;
51+
}
52+
2753
export class MermaidChart {
2854
private clientID: string;
2955
#baseURL!: string;
@@ -300,23 +326,44 @@ export class MermaidChart {
300326
);
301327
return response.data;
302328
} catch (error: unknown) {
303-
if (
304-
error &&
305-
typeof error === 'object' &&
306-
'response' in error &&
307-
error.response &&
308-
typeof error.response === 'object' &&
309-
'status' in error.response &&
310-
error.response.status === 402
311-
) {
312-
const axiosError = error as { response: { status: number; data?: unknown } };
313-
throw new AICreditsLimitExceededError(
314-
typeof axiosError.response.data === 'string'
315-
? axiosError.response.data
316-
: 'AI credits limit exceeded',
317-
);
318-
}
319-
throw error;
329+
throwIfAICreditsExceeded(error);
330+
}
331+
}
332+
333+
/**
334+
* Chat with Mermaid AI about a diagram.
335+
*
336+
* Sends a single user message to the Mermaid AI chat endpoint. The backend
337+
* automatically fetches the full conversation history from the database
338+
* (when `documentChatThreadID` is provided), so callers never need to track
339+
* or resend previous messages.
340+
*
341+
* @param request - The chat request containing the user message and diagram context
342+
* @returns The AI response text and the chat thread ID
343+
* @throws {@link AICreditsLimitExceededError} if AI credits limit is exceeded (HTTP 402)
344+
*/
345+
public async diagramChat(request: DiagramChatRequest): Promise<DiagramChatResponse> {
346+
const { message, documentID, code = '', documentChatThreadID } = request;
347+
348+
try {
349+
const { data } = await this.axios.post<DiagramChatResponse>(URLS.rest.openai.chat, {
350+
messages: [
351+
{ id: uuid(), role: 'user' as const, content: message, experimental_attachments: [] },
352+
],
353+
code,
354+
documentID,
355+
documentChatThreadID,
356+
parentID: null,
357+
autoFetchHistory: true,
358+
});
359+
360+
return {
361+
text: data.text,
362+
documentChatThreadID: data.documentChatThreadID ?? documentChatThreadID,
363+
documentID,
364+
};
365+
} catch (error: unknown) {
366+
throwIfAICreditsExceeded(error);
320367
}
321368
}
322369
}

packages/sdk/src/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,40 @@ export interface RepairDiagramRequest {
9696
userID?: string;
9797
}
9898

99+
/**
100+
* Request parameters for chatting with the Mermaid AI about a diagram.
101+
*/
102+
export interface DiagramChatRequest {
103+
/** The user's chat message / question. */
104+
message: string;
105+
/** The MermaidChart document ID to associate the chat thread with. */
106+
documentID: string;
107+
/** Mermaid diagram code for context. Defaults to an empty string. */
108+
code?: string;
109+
/**
110+
* Existing chat thread ID to continue a conversation.
111+
* Returned from a previous diagramChat() call.
112+
* When provided, the backend automatically fetches the stored conversation
113+
* history from the database so the AI has full context.
114+
*/
115+
documentChatThreadID?: string;
116+
}
117+
118+
/**
119+
* Response from chatting with the Mermaid AI.
120+
*/
121+
export interface DiagramChatResponse {
122+
/** The AI response text, which may contain Mermaid code blocks. */
123+
text: string;
124+
/** Same as the document ID passed in the request. */
125+
documentID: string;
126+
/**
127+
* The chat thread ID created or used for this conversation.
128+
* Pass this back as documentChatThreadID in subsequent calls to continue the thread.
129+
*/
130+
documentChatThreadID?: string;
131+
}
132+
99133
/**
100134
* Response from repairing a diagram.
101135
* Matches OpenAIGenerationResult from collab.

packages/sdk/src/urls.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const URLS = {
4141
},
4242
openai: {
4343
repair: `/rest-api/openai/repair`,
44+
chat: `/rest-api/openai/chat`,
4445
},
4546
},
4647
raw: (document: Pick<MCDocument, 'documentID' | 'major' | 'minor'>, theme: 'light' | 'dark') => {

0 commit comments

Comments
 (0)