@mermalaid/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Plain-English summary: You can share and adapt this noncommercially as long as you give credit and share changes under CC BY-NC-SA.
2
+ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
3
+
4
+ Copyright © 2025–present Dario Novoa (highvoltag3)
5
+
6
+ This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
7
+
8
+ You are free to:
9
+ - Share — copy and redistribute the material in any medium or format
10
+ - Adapt — remix, transform, and build upon the material
11
+
12
+ Under the following terms:
13
+ - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
14
+ - NonCommercial — You may not use the material for commercial purposes.
15
+ - ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license.
16
+
17
+ Full license text available at:
18
+ https://creativecommons.org/licenses/by-nc-sa/4.0/
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @mermalaid/mcp
2
+
3
+ A local [MCP](https://modelcontextprotocol.io) server that bridges an AI agent to a **live
4
+ Mermalaid editor**. The agent can read the current diagram, replace it (changes appear instantly
5
+ in the user's editor), validate syntax, and render the diagram to an image it can see.
6
+
7
+ It speaks MCP to the agent over **stdio**, and hosts a **loopback WebSocket** (`127.0.0.1:7337`)
8
+ that the Mermalaid editor connects to. Nothing leaves the machine.
9
+
10
+ Full guide (install, pairing, tools, security):
11
+ [docs/AGENT_INTEGRATION.md](https://github.com/highvoltag3/mermalaid/blob/main/docs/AGENT_INTEGRATION.md).
12
+
13
+ ## Install / register with an agent
14
+
15
+ ```bash
16
+ claude mcp add mermalaid -- npx -y @mermalaid/mcp
17
+ ```
18
+
19
+ Claude Desktop / Cursor: `"command": "npx", "args": ["-y", "@mermalaid/mcp"]` (see the guide for the full JSON).
20
+
21
+ The server prints the pairing code and bridge URL to stderr on startup. The agent can also fetch
22
+ them with the `get_pairing_code` tool.
23
+
24
+ ## Develop from this repo
25
+
26
+ ```bash
27
+ npm install
28
+ npm run build # compiles to dist/
29
+ npm run dev # tsc --watch
30
+ npm test
31
+ npm run typecheck
32
+ ```
33
+
34
+ Or from the repo root: `npm run mcp:build`.
35
+
36
+ To point an agent at a local build instead of the published package:
37
+
38
+ ```bash
39
+ claude mcp add mermalaid -- node /absolute/path/to/mermalaid/mcp/dist/index.js
40
+ ```
41
+
42
+ ## Tools
43
+
44
+ `get_connection_status`, `get_pairing_code`, `wait_for_editor`, `get_diagram`, `set_diagram`,
45
+ `append_to_diagram`, `validate_diagram`, `render_diagram`, `get_syntax_reference`.
46
+
47
+ ## Resource
48
+
49
+ `mermalaid://diagram/current` (`text/vnd.mermaid`) — the live diagram, readable and subscribable
50
+ (`resources/updated` fires on every change).
51
+
52
+ ## Configuration
53
+
54
+ | Setting | Flag | Env | Default |
55
+ |---|---|---|---|
56
+ | Port | `--port <n>` | `MERMALAID_BRIDGE_PORT` | `7337` (scans → `7340`) |
57
+ | Host | `--host <h>` | `MERMALAID_BRIDGE_HOST` | `127.0.0.1` |
58
+ | Extra origins | `--origins <csv>` | `MERMALAID_BRIDGE_ORIGINS` | (none) |
59
+ | Allow localhost dev origins | `--dev` | `MERMALAID_BRIDGE_DEV` | off |
60
+
61
+ ## Security model
62
+
63
+ Loopback bind + a fresh per-session pairing code (required before any tool touches the diagram) +
64
+ an Origin allowlist on the WebSocket handshake. Designed for a single local user. See the guide
65
+ for details.
66
+
67
+ ## License
68
+
69
+ CC-BY-NC-SA-4.0, same as Mermalaid.
package/dist/config.js ADDED
@@ -0,0 +1,45 @@
1
+ import { BRIDGE_PORT_FALLBACKS, DEFAULT_BRIDGE_HOST, DEFAULT_HEARTBEAT_INTERVAL_MS, } from './protocol.js';
2
+ function parsePositiveInt(value) {
3
+ if (!value)
4
+ return undefined;
5
+ const n = Number.parseInt(value, 10);
6
+ return Number.isFinite(n) && n > 0 ? n : undefined;
7
+ }
8
+ function readFlag(argv, flag) {
9
+ const i = argv.indexOf(flag);
10
+ if (i !== -1 && i + 1 < argv.length)
11
+ return argv[i + 1];
12
+ const withEq = argv.find((a) => a.startsWith(`${flag}=`));
13
+ return withEq ? withEq.slice(flag.length + 1) : undefined;
14
+ }
15
+ /**
16
+ * Resolve bridge configuration from CLI flags and environment variables.
17
+ * `--port` / MERMALAID_BRIDGE_PORT pins a single port (no fallback scan).
18
+ * `--origins` / MERMALAID_BRIDGE_ORIGINS is a comma-separated origin allowlist extension.
19
+ */
20
+ export function loadConfig(argv = process.argv.slice(2), env = process.env) {
21
+ const port = parsePositiveInt(readFlag(argv, '--port') ?? env.MERMALAID_BRIDGE_PORT);
22
+ const host = readFlag(argv, '--host') ?? env.MERMALAID_BRIDGE_HOST ?? DEFAULT_BRIDGE_HOST;
23
+ const originsRaw = readFlag(argv, '--origins') ?? env.MERMALAID_BRIDGE_ORIGINS ?? '';
24
+ const extraOrigins = originsRaw
25
+ .split(',')
26
+ .map((o) => o.trim())
27
+ .filter((o) => o.length > 0);
28
+ const allowDevOrigins = argv.includes('--dev') || Boolean(env.MERMALAID_BRIDGE_DEV);
29
+ return {
30
+ host,
31
+ ports: port ? [port] : [...BRIDGE_PORT_FALLBACKS],
32
+ extraOrigins,
33
+ allowDevOrigins,
34
+ heartbeatIntervalMs: DEFAULT_HEARTBEAT_INTERVAL_MS,
35
+ maxPayloadBytes: 8 * 1024 * 1024,
36
+ timeouts: {
37
+ getDiagramMs: 3_000,
38
+ setDiagramMs: 8_000,
39
+ validateMs: 5_000,
40
+ renderSvgMs: 15_000,
41
+ renderPngMs: 20_000,
42
+ },
43
+ };
44
+ }
45
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,6BAA6B,GAC9B,MAAM,eAAe,CAAA;AAsBtB,SAAS,gBAAgB,CAAC,KAAyB;IACjD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACpC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACpD,CAAC;AAED,SAAS,QAAQ,CAAC,IAAc,EAAE,IAAY;IAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAA;IACzD,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,OAAiB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EACtC,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,IAAI,GAAG,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,qBAAqB,CAAC,CAAA;IACpF,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,qBAAqB,IAAI,mBAAmB,CAAA;IAEzF,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,GAAG,CAAC,wBAAwB,IAAI,EAAE,CAAA;IACpF,MAAM,YAAY,GAAG,UAAU;SAC5B,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAE9B,MAAM,eAAe,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;IAEnF,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC;QACjD,YAAY;QACZ,eAAe;QACf,mBAAmB,EAAE,6BAA6B;QAClD,eAAe,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;QAChC,QAAQ,EAAE;YACR,YAAY,EAAE,KAAK;YACnB,YAAY,EAAE,KAAK;YACnB,UAAU,EAAE,KAAK;YACjB,WAAW,EAAE,MAAM;YACnB,WAAW,EAAE,MAAM;SACpB;KACF,CAAA;AACH,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { loadConfig } from './config.js';
7
+ import { makeOriginChecker } from './origins.js';
8
+ import { formatPairingCode, generatePairingCode } from './pairing.js';
9
+ import { registerDiagramResource } from './resources.js';
10
+ import { BridgeSession } from './session.js';
11
+ import { registerTools } from './tools.js';
12
+ import { BridgeWsServer } from './wsServer.js';
13
+ const SERVER_NAME = 'mermalaid-mcp';
14
+ function readVersion() {
15
+ try {
16
+ const pkgUrl = new URL('../package.json', import.meta.url);
17
+ const pkg = JSON.parse(readFileSync(fileURLToPath(pkgUrl), 'utf8'));
18
+ return pkg.version ?? '0.0.0';
19
+ }
20
+ catch {
21
+ return '0.0.0';
22
+ }
23
+ }
24
+ // The MCP protocol owns stdout — everything human-facing must go to stderr.
25
+ function logStderr(line) {
26
+ process.stderr.write(`${line}\n`);
27
+ }
28
+ async function main() {
29
+ const config = loadConfig();
30
+ const version = readVersion();
31
+ const pairingCode = generatePairingCode();
32
+ const session = new BridgeSession({
33
+ pairingCode,
34
+ serverInfo: { name: SERVER_NAME, version },
35
+ heartbeatIntervalMs: config.heartbeatIntervalMs,
36
+ timeouts: config.timeouts,
37
+ });
38
+ const ws = new BridgeWsServer({
39
+ host: config.host,
40
+ ports: config.ports,
41
+ allowOrigin: makeOriginChecker({
42
+ extraOrigins: config.extraOrigins,
43
+ allowDevOrigins: config.allowDevOrigins,
44
+ }),
45
+ heartbeatIntervalMs: config.heartbeatIntervalMs,
46
+ maxPayloadBytes: config.maxPayloadBytes,
47
+ }, {
48
+ onMessage: (conn, msg) => session.handleMessage(conn, msg),
49
+ onClose: (conn) => session.handleClose(conn),
50
+ });
51
+ const port = await ws.start();
52
+ const server = new McpServer({ name: SERVER_NAME, version });
53
+ registerTools(server, { session, pairingCode, port });
54
+ registerDiagramResource(server, session);
55
+ const transport = new StdioServerTransport();
56
+ await server.connect(transport);
57
+ logStderr('');
58
+ logStderr(`[${SERVER_NAME}] v${version} ready.`);
59
+ logStderr(`[${SERVER_NAME}] Bridge listening on ws://${config.host}:${port}`);
60
+ logStderr(`[${SERVER_NAME}] Pairing code: ${formatPairingCode(pairingCode)}`);
61
+ logStderr(`[${SERVER_NAME}] In Mermalaid, open the "AI Agent" panel and enter the code to connect.`);
62
+ logStderr('');
63
+ let shuttingDown = false;
64
+ const shutdown = async () => {
65
+ if (shuttingDown)
66
+ return;
67
+ shuttingDown = true;
68
+ session.dispose();
69
+ await ws.stop();
70
+ await server.close().catch(() => { });
71
+ process.exit(0);
72
+ };
73
+ process.on('SIGINT', () => void shutdown());
74
+ process.on('SIGTERM', () => void shutdown());
75
+ }
76
+ main().catch((err) => {
77
+ logStderr(`[${SERVER_NAME}] Fatal: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
78
+ process.exit(1);
79
+ });
80
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ // Origin allowlist for the localhost WebSocket bridge.
2
+ //
3
+ // Browsers always send a truthful `Origin` header on a WebSocket handshake and page
4
+ // scripts cannot forge it, so this list blocks any web page not served from a known
5
+ // Mermalaid origin from driving the socket — defense-in-depth on top of the pairing code.
6
+ const PRODUCTION_ORIGINS = [
7
+ 'https://mermalaid.com',
8
+ 'https://www.mermalaid.com',
9
+ // Tauri desktop webview (macOS/Linux use tauri://localhost; Windows uses http://tauri.localhost)
10
+ 'tauri://localhost',
11
+ 'http://tauri.localhost',
12
+ ];
13
+ // Generic Vite dev/preview ports. Off by default (a malicious page on localhost:5173 would
14
+ // otherwise pass the Origin layer); opt in with MERMALAID_BRIDGE_DEV when developing Mermalaid.
15
+ const DEV_ORIGINS = [
16
+ 'http://localhost:5173',
17
+ 'http://127.0.0.1:5173',
18
+ 'http://localhost:4173',
19
+ 'http://127.0.0.1:4173',
20
+ ];
21
+ export function makeOriginChecker(options = {}) {
22
+ const allowed = new Set([
23
+ ...PRODUCTION_ORIGINS,
24
+ ...(options.allowDevOrigins ? DEV_ORIGINS : []),
25
+ ...(options.extraOrigins ?? []),
26
+ ]);
27
+ return (origin) => {
28
+ // Non-browser MCP tooling and some webviews omit Origin entirely. Allow the empty case —
29
+ // the pairing code still gates binding, and these clients are already local (loopback bind).
30
+ if (!origin)
31
+ return true;
32
+ return allowed.has(origin);
33
+ };
34
+ }
35
+ export { PRODUCTION_ORIGINS, DEV_ORIGINS };
36
+ //# sourceMappingURL=origins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"origins.js","sourceRoot":"","sources":["../src/origins.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,oFAAoF;AACpF,oFAAoF;AACpF,0FAA0F;AAC1F,MAAM,kBAAkB,GAAG;IACzB,uBAAuB;IACvB,2BAA2B;IAC3B,iGAAiG;IACjG,mBAAmB;IACnB,wBAAwB;CACzB,CAAA;AAED,2FAA2F;AAC3F,gGAAgG;AAChG,MAAM,WAAW,GAAG;IAClB,uBAAuB;IACvB,uBAAuB;IACvB,uBAAuB;IACvB,uBAAuB;CACxB,CAAA;AAOD,MAAM,UAAU,iBAAiB,CAC/B,UAAgC,EAAE;IAElC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC;QACtB,GAAG,kBAAkB;QACrB,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;KAChC,CAAC,CAAA;IACF,OAAO,CAAC,MAAM,EAAE,EAAE;QAChB,yFAAyF;QACzF,6FAA6F;QAC7F,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAA;QACxB,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,CAAA"}
@@ -0,0 +1,32 @@
1
+ import { randomInt, timingSafeEqual } from 'node:crypto';
2
+ // Crockford-style base32 without visually ambiguous characters (no 0/1/I/L/O/U).
3
+ // The set is unambiguous by construction, so pairing codes are safe to read aloud/type.
4
+ const ALPHABET = '23456789ABCDEFGHJKMNPQRSTVWXYZ';
5
+ /** Generate a fresh, session-only pairing code (default 8 chars ≈ 40 bits). */
6
+ export function generatePairingCode(length = 8) {
7
+ let out = '';
8
+ for (let i = 0; i < length; i++) {
9
+ out += ALPHABET[randomInt(0, ALPHABET.length)];
10
+ }
11
+ return out;
12
+ }
13
+ /** Uppercase and strip anything outside the alphabet (dashes, spaces) for comparison. */
14
+ export function normalizePairingCode(input) {
15
+ return input.toUpperCase().replace(/[^0-9A-Z]/g, '');
16
+ }
17
+ /** Human-friendly grouping, e.g. `K7RM9QX2` -> `K7RM-9QX2`. */
18
+ export function formatPairingCode(code) {
19
+ const c = normalizePairingCode(code);
20
+ if (c.length <= 4)
21
+ return c;
22
+ return `${c.slice(0, 4)}-${c.slice(4)}`;
23
+ }
24
+ /** Constant-time comparison of two pairing codes after normalization. */
25
+ export function pairingCodesMatch(a, b) {
26
+ const na = Buffer.from(normalizePairingCode(a), 'utf8');
27
+ const nb = Buffer.from(normalizePairingCode(b), 'utf8');
28
+ if (na.length === 0 || na.length !== nb.length)
29
+ return false;
30
+ return timingSafeEqual(na, nb);
31
+ }
32
+ //# sourceMappingURL=pairing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.js","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAExD,iFAAiF;AACjF,wFAAwF;AACxF,MAAM,QAAQ,GAAG,gCAAgC,CAAA;AAEjD,+EAA+E;AAC/E,MAAM,UAAU,mBAAmB,CAAC,MAAM,GAAG,CAAC;IAC5C,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,GAAG,IAAI,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IAChD,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAA;AACtD,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,CAAC,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAA;IACpC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IAC3B,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;AACzC,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,iBAAiB,CAAC,CAAS,EAAE,CAAS;IACpD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACvD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACvD,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAC5D,OAAO,eAAe,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;AAChC,CAAC"}
@@ -0,0 +1,53 @@
1
+ // Wire protocol shared between the `mermalaid-mcp` server and the Mermalaid editor.
2
+ //
3
+ // SOURCE OF TRUTH. This file is duplicated verbatim to `src/agentBridge/protocol.ts`
4
+ // so the browser bundle (Vite, bundler resolution) and the Node server (tsc, NodeNext
5
+ // resolution) each get a self-contained copy. A drift test asserts the two stay identical.
6
+ // Keep this file DEPENDENCY-FREE (pure types + consts + guards) — no imports — so it is
7
+ // valid under both module resolution modes.
8
+ export const PROTOCOL_VERSION = 1;
9
+ export const DEFAULT_BRIDGE_HOST = '127.0.0.1';
10
+ export const DEFAULT_BRIDGE_PORT = 7337;
11
+ /** Ports scanned in order when the default is already taken. */
12
+ export const BRIDGE_PORT_FALLBACKS = [7337, 7338, 7339, 7340];
13
+ export const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
14
+ /** Discriminator values for every message on the wire. */
15
+ export const MessageType = {
16
+ // editor -> server
17
+ Hello: 'hello',
18
+ State: 'state',
19
+ SetDiagramResult: 'setDiagramResult',
20
+ GetDiagramResult: 'getDiagramResult',
21
+ ValidateResult: 'validateResult',
22
+ RenderResult: 'renderResult',
23
+ Pong: 'pong',
24
+ // server -> editor
25
+ Welcome: 'welcome',
26
+ Reject: 'reject',
27
+ Superseded: 'superseded',
28
+ SetDiagram: 'setDiagram',
29
+ GetDiagram: 'getDiagram',
30
+ Validate: 'validate',
31
+ Render: 'render',
32
+ Ping: 'ping',
33
+ };
34
+ /** Parse a raw frame into a BridgeMessage, or null if it is not a well-formed envelope. */
35
+ export function parseBridgeMessage(raw) {
36
+ let obj;
37
+ try {
38
+ obj = JSON.parse(raw);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ if (!obj || typeof obj !== 'object')
44
+ return null;
45
+ const m = obj;
46
+ if (typeof m.type !== 'string' || typeof m.v !== 'number')
47
+ return null;
48
+ return m;
49
+ }
50
+ export function serializeBridgeMessage(msg) {
51
+ return JSON.stringify(msg);
52
+ }
53
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,EAAE;AACF,qFAAqF;AACrF,sFAAsF;AACtF,2FAA2F;AAC3F,wFAAwF;AACxF,4CAA4C;AAE5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAA;AAEjC,MAAM,CAAC,MAAM,mBAAmB,GAAG,WAAW,CAAA;AAC9C,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAA;AACvC,gEAAgE;AAChE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;AAE7D,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAA;AAEnD,0DAA0D;AAC1D,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,mBAAmB;IACnB,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,gBAAgB,EAAE,kBAAkB;IACpC,gBAAgB,EAAE,kBAAkB;IACpC,cAAc,EAAE,gBAAgB;IAChC,YAAY,EAAE,cAAc;IAC5B,IAAI,EAAE,MAAM;IACZ,mBAAmB;IACnB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,MAAM;CACJ,CAAA;AA6KV,2FAA2F;AAC3F,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,GAAY,CAAA;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAChD,MAAM,CAAC,GAAG,GAA8B,CAAA;IACxC,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IACtE,OAAO,CAA6B,CAAA;AACtC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAkB;IACvD,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;AAC5B,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { SubscribeRequestSchema, UnsubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
+ export const DIAGRAM_RESOURCE_URI = 'mermalaid://diagram/current';
3
+ const DIAGRAM_MIME = 'text/vnd.mermaid';
4
+ /**
5
+ * Exposes the live diagram as an MCP resource so users can @-mention / attach it in clients
6
+ * like Claude Desktop. The high-level McpServer wires resources/read + list but not
7
+ * subscribe, so this also registers subscribe/unsubscribe and pushes `resources/updated`
8
+ * whenever the diagram changes.
9
+ */
10
+ export function registerDiagramResource(server, session) {
11
+ server.registerResource('current-diagram', DIAGRAM_RESOURCE_URI, {
12
+ title: 'Current Mermalaid diagram',
13
+ description: 'The Mermaid source currently open in the connected Mermalaid editor. Reflects live edits from the user and the agent.',
14
+ mimeType: DIAGRAM_MIME,
15
+ }, async (uri) => {
16
+ const cached = session.getCachedDiagram();
17
+ const text = cached
18
+ ? cached.code
19
+ : session.isPaired
20
+ ? '%% The Mermalaid editor is connected but has not sent its diagram yet.'
21
+ : '%% No Mermalaid editor is connected. Open Mermalaid and pair the AI Agent panel.';
22
+ return { contents: [{ uri: uri.toString(), mimeType: DIAGRAM_MIME, text }] };
23
+ });
24
+ const subscribers = new Set();
25
+ server.server.registerCapabilities({ resources: { subscribe: true } });
26
+ server.server.setRequestHandler(SubscribeRequestSchema, async (request) => {
27
+ subscribers.add(request.params.uri);
28
+ return {};
29
+ });
30
+ server.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
31
+ subscribers.delete(request.params.uri);
32
+ return {};
33
+ });
34
+ session.onStateChange(() => {
35
+ if (!subscribers.has(DIAGRAM_RESOURCE_URI))
36
+ return;
37
+ void server.server.sendResourceUpdated({ uri: DIAGRAM_RESOURCE_URI }).catch(() => { });
38
+ });
39
+ }
40
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAA;AAGrG,MAAM,CAAC,MAAM,oBAAoB,GAAG,6BAA6B,CAAA;AACjE,MAAM,YAAY,GAAG,kBAAkB,CAAA;AAEvC;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAiB,EAAE,OAAsB;IAC/E,MAAM,CAAC,gBAAgB,CACrB,iBAAiB,EACjB,oBAAoB,EACpB;QACE,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,uHAAuH;QACzH,QAAQ,EAAE,YAAY;KACvB,EACD,KAAK,EAAE,GAAG,EAAE,EAAE;QACZ,MAAM,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAA;QACzC,MAAM,IAAI,GAAG,MAAM;YACjB,CAAC,CAAC,MAAM,CAAC,IAAI;YACb,CAAC,CAAC,OAAO,CAAC,QAAQ;gBAChB,CAAC,CAAC,wEAAwE;gBAC1E,CAAC,CAAC,kFAAkF,CAAA;QACxF,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;IAC9E,CAAC,CACF,CAAA;IAED,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAA;IACrC,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;IACtE,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACxE,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACnC,OAAO,EAAE,CAAA;IACX,CAAC,CAAC,CAAA;IACF,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,wBAAwB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAC1E,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACtC,OAAO,EAAE,CAAA;IACX,CAAC,CAAC,CAAA;IAEF,OAAO,CAAC,aAAa,CAAC,GAAG,EAAE;QACzB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,oBAAoB,CAAC;YAAE,OAAM;QAClD,KAAK,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,EAAE,GAAG,EAAE,oBAAoB,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACvF,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,245 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { MessageType, PROTOCOL_VERSION, } from './protocol.js';
3
+ import { pairingCodesMatch } from './pairing.js';
4
+ export class BridgeNotConnectedError extends Error {
5
+ constructor() {
6
+ super('No Mermalaid editor is connected');
7
+ this.name = 'BridgeNotConnectedError';
8
+ }
9
+ }
10
+ export class BridgeTimeoutError extends Error {
11
+ constructor(op, timeoutMs) {
12
+ super(`Editor did not respond to "${op}" within ${timeoutMs}ms`);
13
+ this.name = 'BridgeTimeoutError';
14
+ }
15
+ }
16
+ export class BridgeDisconnectedError extends Error {
17
+ constructor(reason) {
18
+ super(reason);
19
+ this.name = 'BridgeDisconnectedError';
20
+ }
21
+ }
22
+ /**
23
+ * Owns the single bound-editor relationship: pairing handshake, newest-wins supersede,
24
+ * the cached editor state, and the request/response correlation used by the MCP tools.
25
+ */
26
+ export class BridgeSession {
27
+ opts;
28
+ editor = null;
29
+ editorInfo = null;
30
+ capabilities = null;
31
+ cached = null;
32
+ pending = new Map();
33
+ waiters = [];
34
+ stateChangeListeners = [];
35
+ constructor(opts) {
36
+ this.opts = opts;
37
+ }
38
+ get isPaired() {
39
+ return this.editor !== null;
40
+ }
41
+ /** Subscribe to cached-diagram changes (bind, unbind, and each state push). */
42
+ onStateChange(listener) {
43
+ this.stateChangeListeners.push(listener);
44
+ }
45
+ emitStateChange() {
46
+ for (const listener of this.stateChangeListeners) {
47
+ try {
48
+ listener();
49
+ }
50
+ catch {
51
+ // a listener must not break session handling
52
+ }
53
+ }
54
+ }
55
+ /** The last known diagram, or null if none has been received yet. */
56
+ getCachedDiagram() {
57
+ if (!this.cached)
58
+ return null;
59
+ const c = this.cached;
60
+ return { code: c.code, rev: c.rev, valid: c.valid, error: c.error };
61
+ }
62
+ // ---- inbound (called by the WS server) ----
63
+ handleMessage(conn, msg) {
64
+ if (msg.type === MessageType.Hello) {
65
+ this.handleHello(conn, msg);
66
+ return;
67
+ }
68
+ // Only the currently-bound editor may drive session state / resolve requests.
69
+ if (conn !== this.editor)
70
+ return;
71
+ switch (msg.type) {
72
+ case MessageType.State:
73
+ this.cacheState(msg);
74
+ break;
75
+ case MessageType.SetDiagramResult:
76
+ case MessageType.GetDiagramResult:
77
+ case MessageType.ValidateResult:
78
+ case MessageType.RenderResult:
79
+ case MessageType.Pong:
80
+ if (msg.id)
81
+ this.resolvePending(msg.id, msg);
82
+ break;
83
+ default:
84
+ break;
85
+ }
86
+ }
87
+ handleClose(conn) {
88
+ if (conn !== this.editor)
89
+ return;
90
+ this.editor = null;
91
+ this.editorInfo = null;
92
+ this.capabilities = null;
93
+ this.cached = null;
94
+ this.failAllPending(new BridgeDisconnectedError('Editor disconnected'));
95
+ this.emitStateChange();
96
+ }
97
+ handleHello(conn, msg) {
98
+ if (typeof msg.v !== 'number' || msg.v !== PROTOCOL_VERSION) {
99
+ this.rejectConn(conn, 'version_mismatch', `Unsupported protocol version ${msg.v}; server speaks ${PROTOCOL_VERSION}.`);
100
+ return;
101
+ }
102
+ if (!msg.pairingCode || !pairingCodesMatch(msg.pairingCode, this.opts.pairingCode)) {
103
+ this.rejectConn(conn, 'bad_pairing_code', 'Invalid pairing code.');
104
+ return;
105
+ }
106
+ // Newest-wins supersede: an existing editor is bumped in favor of the new one.
107
+ if (this.editor && this.editor !== conn) {
108
+ this.editor.send({
109
+ v: PROTOCOL_VERSION,
110
+ type: MessageType.Superseded,
111
+ message: 'Another Mermalaid tab or window took over the agent bridge.',
112
+ });
113
+ this.editor.close(4002, 'superseded');
114
+ this.failAllPending(new BridgeDisconnectedError('Editor superseded by a newer connection'));
115
+ }
116
+ this.editor = conn;
117
+ this.editorInfo = msg.clientInfo ?? null;
118
+ this.capabilities = msg.capabilities ?? null;
119
+ this.cached = null; // a fresh `state` push follows welcome
120
+ this.emitStateChange(); // notify resource subscribers the cached diagram was reset
121
+ conn.send({
122
+ v: PROTOCOL_VERSION,
123
+ type: MessageType.Welcome,
124
+ sessionId: randomUUID(),
125
+ serverInfo: this.opts.serverInfo,
126
+ protocolVersion: PROTOCOL_VERSION,
127
+ heartbeatIntervalMs: this.opts.heartbeatIntervalMs,
128
+ });
129
+ this.resolveWaiters();
130
+ }
131
+ rejectConn(conn, reason, message) {
132
+ conn.send({ v: PROTOCOL_VERSION, type: MessageType.Reject, reason, message });
133
+ conn.close(4001, reason);
134
+ }
135
+ cacheState(msg) {
136
+ this.cached = {
137
+ rev: msg.rev,
138
+ code: msg.code,
139
+ valid: msg.valid,
140
+ error: msg.error,
141
+ source: msg.source,
142
+ updatedAt: Date.now(),
143
+ };
144
+ this.emitStateChange();
145
+ }
146
+ // ---- request/response correlation ----
147
+ request(payload, timeoutMs) {
148
+ const editor = this.editor;
149
+ if (!editor)
150
+ return Promise.reject(new BridgeNotConnectedError());
151
+ const id = randomUUID();
152
+ return new Promise((resolve, reject) => {
153
+ const timer = setTimeout(() => {
154
+ this.pending.delete(id);
155
+ reject(new BridgeTimeoutError(payload.type, timeoutMs));
156
+ }, timeoutMs);
157
+ timer.unref?.();
158
+ this.pending.set(id, {
159
+ resolve: resolve,
160
+ reject,
161
+ timer,
162
+ });
163
+ editor.send({ ...payload, v: PROTOCOL_VERSION, id });
164
+ });
165
+ }
166
+ resolvePending(id, msg) {
167
+ const p = this.pending.get(id);
168
+ if (!p)
169
+ return;
170
+ clearTimeout(p.timer);
171
+ this.pending.delete(id);
172
+ p.resolve(msg);
173
+ }
174
+ failAllPending(err) {
175
+ for (const p of this.pending.values()) {
176
+ clearTimeout(p.timer);
177
+ p.reject(err);
178
+ }
179
+ this.pending.clear();
180
+ }
181
+ // ---- operations used by the MCP tools ----
182
+ async setDiagram(code, reason) {
183
+ return this.request({ type: MessageType.SetDiagram, code, mode: 'replace', reason }, this.opts.timeouts.setDiagramMs);
184
+ }
185
+ async getDiagram(fresh = false) {
186
+ const cached = this.getCachedDiagram();
187
+ if (!fresh && cached)
188
+ return cached;
189
+ const res = await this.request({ type: MessageType.GetDiagram }, this.opts.timeouts.getDiagramMs);
190
+ return { code: res.code, rev: res.rev, valid: res.valid, error: res.error };
191
+ }
192
+ async validate(code) {
193
+ return this.request({ type: MessageType.Validate, code }, this.opts.timeouts.validateMs);
194
+ }
195
+ async render(format, opts = {}) {
196
+ const timeout = format === 'png' ? this.opts.timeouts.renderPngMs : this.opts.timeouts.renderSvgMs;
197
+ return this.request({ type: MessageType.Render, format, code: opts.code, scale: opts.scale, theme: opts.theme }, timeout);
198
+ }
199
+ /** Resolve once an editor is paired, or reject on timeout. */
200
+ waitForEditor(timeoutMs) {
201
+ if (this.isPaired)
202
+ return Promise.resolve();
203
+ return new Promise((resolve, reject) => {
204
+ const waiter = {
205
+ resolve,
206
+ reject,
207
+ timer: setTimeout(() => {
208
+ this.waiters = this.waiters.filter((w) => w !== waiter);
209
+ reject(new BridgeTimeoutError('wait_for_editor', timeoutMs));
210
+ }, timeoutMs),
211
+ };
212
+ waiter.timer.unref?.();
213
+ this.waiters.push(waiter);
214
+ });
215
+ }
216
+ resolveWaiters() {
217
+ const waiters = this.waiters;
218
+ this.waiters = [];
219
+ for (const w of waiters) {
220
+ clearTimeout(w.timer);
221
+ w.resolve();
222
+ }
223
+ }
224
+ status() {
225
+ return {
226
+ paired: this.isPaired,
227
+ editorInfo: this.editorInfo,
228
+ capabilities: this.capabilities,
229
+ hasCachedState: this.cached !== null,
230
+ lastRevision: this.cached?.rev ?? null,
231
+ lastValid: this.cached?.valid ?? null,
232
+ };
233
+ }
234
+ /** Reject outstanding work and drop the editor — used on shutdown. */
235
+ dispose() {
236
+ this.failAllPending(new BridgeDisconnectedError('Server shutting down'));
237
+ for (const w of this.waiters) {
238
+ clearTimeout(w.timer);
239
+ w.reject(new BridgeDisconnectedError('Server shutting down'));
240
+ }
241
+ this.waiters = [];
242
+ this.editor = null;
243
+ }
244
+ }
245
+ //# sourceMappingURL=session.js.map