feat(mcp-server): serve the bundled draw.io same-origin behind an api token

GET /drawio/<path> serves dist/drawio with a MIME table, a day of
caching and nosniff; paths are normalized and never reach WEB-INF or
META-INF. The preview embeds /drawio/index.html when the copy exists and
DRAWIO_BASE_URL is unset, else the external draw.io as before (and
start_session says so). Every /api request must carry the per-process
X-Drawio-Token the page gets in its HTML; pages send
frame-ancestors 'self' and nosniff.
This commit is contained in:
dayuan.jiang
2026-10-11 20:56:07 +09:00
parent 38fe675d6b
commit d415f19cf5
7 changed files with 341 additions and 30 deletions
+4 -2
View File
@@ -40,7 +40,7 @@
</button>
</div>
</div>
<iframe id="drawio" src="{{DRAWIO_URL}}/?embed=1&proto=json&spin=1&libraries=1&noSaveBtn=1&noExitBtn=1&saveAndExit=0&themes=1&{{DRAWIO_PARAMS}}" title="draw.io editor"></iframe>
<iframe id="drawio" src="{{DRAWIO_URL}}?embed=1&proto=json&spin=1&libraries=1&noSaveBtn=1&noExitBtn=1&saveAndExit=0&themes=1&{{DRAWIO_PARAMS}}" title="draw.io editor"></iframe>
</div>
<div id="notice" role="status"></div>
<div id="history-modal">
@@ -85,7 +85,9 @@
</div>
<script>
const sessionId = {{SESSION_JSON}};
const DRAWIO_ORIGIN = {{ORIGIN_JSON}};
// Empty when draw.io is served from this origin
const DRAWIO_ORIGIN = {{ORIGIN_JSON}} || location.origin;
const API_TOKEN = {{TOKEN_JSON}};
</script>
<script>
{{SCRIPT}}
+8 -6
View File
@@ -1,4 +1,6 @@
const iframe = document.getElementById('drawio');
// Every API request carries the token the server wrote into this page
const api = (url, init = {}) => fetch(url, { ...init, headers: { ...(init.headers || {}), 'X-Drawio-Token': API_TOKEN } });
let currentVersion = 0, isReady = false, pendingXml = null, lastXml = null;
// The server state this tab is in step with (see stateId in http-server.ts);
// null until the first poll
@@ -82,7 +84,7 @@ window.addEventListener('message', (e) => {
// result: a poll answered before that still sees the
// request and would start the same export again.
const seq = msg.message.mcpExport;
fetch('/api/state', {
api('/api/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, exportData: d, exportId: mcpExportId })
@@ -126,7 +128,7 @@ window.addEventListener('message', (e) => {
const t = thumbExport;
if (!t || msg.message.thumbExport !== t.n || latestXml !== t.xml) return;
thumbExport = null;
fetch('/api/history-svg', {
api('/api/history-svg', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, svg, stateId: t.stateId, version: t.version })
@@ -184,7 +186,7 @@ async function pushState(xml, svg = '', baseVersion = currentVersion, source = '
if (!sessionId) return;
pushesInFlight.push(xml);
try {
const r = await fetch('/api/state', {
const r = await api('/api/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, xml, svg, baseVersion, source, stateId: sid })
@@ -282,7 +284,7 @@ async function poll() {
if (!sessionId) return;
const seq = ++pollSeq;
try {
const r = await fetch('/api/state?sessionId=' + encodeURIComponent(sessionId));
const r = await api('/api/state?sessionId=' + encodeURIComponent(sessionId));
if (!r.ok) return;
const s = await r.json();
// An older answer than one already handled (the interval, the 409
@@ -469,7 +471,7 @@ historyBtn.onclick = async () => {
// A list for a state the server recreated meanwhile has old ids
const sid = stateId;
try {
const r = await fetch('/api/history?sessionId=' + encodeURIComponent(sessionId));
const r = await api('/api/history?sessionId=' + encodeURIComponent(sessionId));
if (r.ok) {
const d = await r.json();
if (sid !== stateId) return;
@@ -529,7 +531,7 @@ restoreBtn.onclick = async () => {
restoreBtn.disabled = true;
restoreBtn.textContent = 'Restoring...';
try {
const r = await fetch('/api/restore', {
const r = await api('/api/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, id: selectedId, stateId: historyStateId })