@mindstone/mcp-server-microsoft-teams 0.3.0 → 0.3.2
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/CHANGELOG.md +27 -0
- package/README.md +9 -1
- package/dist/resources/compose-message-template.d.ts +1 -1
- package/dist/resources/compose-message-template.js +100 -22
- package/dist/untrusted-content.d.ts +3 -3
- package/dist/untrusted-content.js +27 -16
- package/package.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable changes to this connector will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.2] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Cards keep their send state in every copy and after a restart; a blocked send says why without a Retry that can't work.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Compose cards hide Retry when sending is blocked by your settings, your organization or security policy. Cancel stays available; a stopped server still offers Retry.
|
|
16
|
+
- Send requests declare the host-neutral `_meta: { 'mcp-app/cardOutcome': 'sent' }` outcome.
|
|
17
|
+
- Compose cards keep their sent status across copies and reloads in supporting hosts. Reloaded cards show the send time without the sent contents. The sender keeps its posted contents even if confirmation arrives after a lost reply; malformed host timestamps omit the date.
|
|
18
|
+
- After an unconfirmed send, the check-before-resending notice stays visible while editing and returns on Reopen. Cancel keeps the same reminder. Calls blocked before sending, including `not-dispatched` failures, still say "Nothing sent" unless an earlier attempt may have gone through.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- The compose form's wording no longer names a particular host app: the lost-reply notice now starts "No confirmation came back in time…".
|
|
23
|
+
|
|
24
|
+
## [0.3.1] - 2026-08-10
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Re-sync the vendored envelope helper with the hardened canonical: attribute-bearing close tags and spoofed open tags escaped.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Re-synced the vendored `<untrusted-content>` envelope helper with the canonical hardened reference: attribute-bearing close-tag variants (`</untrusted-content foo>`) and spoofed open tags inside wrapped content are now escaped, closing an envelope-breakout gap an LLM parser could read as an envelope boundary.
|
|
33
|
+
|
|
7
34
|
## [0.3.0] - 2026-08-07
|
|
8
35
|
|
|
9
36
|
### Changed
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Microsoft 365 Teams MCP server — list and read Teams chats and channel message
|
|
|
9
9
|
|
|
10
10
|
## Status
|
|
11
11
|
|
|
12
|
-
- **Version:** [0.3.
|
|
12
|
+
- **Version:** [0.3.2](./CHANGELOG.md) · [npm](https://www.npmjs.com/package/@mindstone/mcp-server-microsoft-teams)
|
|
13
13
|
- **Auth:** OAuth (host-orchestrated, shared with [`mcp-server-microsoft-mail`](../microsoft-mail/)) ([`MS_CLIENT_ID`](./server.json))
|
|
14
14
|
- **Tools:** [17](./src/tools.ts) (chats, messages, teams, channels, users, search, presence)
|
|
15
15
|
- **Surface:** cloud-api
|
|
@@ -188,6 +188,14 @@ Sign in via [`@mindstone/mcp-server-microsoft-mail`](../microsoft-mail/)'s `auth
|
|
|
188
188
|
|
|
189
189
|
Some Teams Graph APIs may require tenant admin approval. Tools marked with a required permission check the connected account's token up front and return actionable reconnect guidance when the permission is missing (under Microsoft's managed consent policy, `ChannelMessage.Read.All` is admin-consent-gated in most tenants).
|
|
190
190
|
|
|
191
|
+
### Compose send status
|
|
192
|
+
|
|
193
|
+
In hosts that remember send status, sending a message closes its other compose copies. Reloaded cards show "Message sent" with the send time and hide Reopen because they do not have the sent contents. The copy used to send keeps its posted contents in a read-only sent view, including when confirmation follows a lost reply. After an error or block reply, a later sent status from another copy shows only the sent bar, without this copy's contents. While a send is in progress, other copies show “Sending…” with Send, Retry, Cancel and Save draft (where available) disabled. Replies to this copy’s own requests keep those controls locked until the other send ends. If the send ends without a confirmed outcome, those copies return to editing and keep any earlier uncertainty. A copy waiting for its own Send reply keeps its sending view. Sent views stay read-only, and uncertainty remains until a send is confirmed. After an error or missing confirmation, check whether the message was sent before using Retry; the notice stays visible while editing and returns on Reopen. Collapsing the card does not resolve that uncertainty. Policy-block explanations stay visible while editing with Send disabled. Cancel says "Nothing sent" only when no send may have gone through, including a `not-dispatched` failure before the request left the device.
|
|
194
|
+
|
|
195
|
+
When sending is blocked by your settings, your organization or security policy, the card hides Retry and keeps Cancel. If the server is stopped, Retry remains available.
|
|
196
|
+
|
|
197
|
+
Send requests declare `_meta: { 'mcp-app/cardOutcome': 'sent' }` to supporting hosts.
|
|
198
|
+
|
|
191
199
|
## Security notes
|
|
192
200
|
|
|
193
201
|
- No authentication tool of its own; sign-in is delegated to [`@mindstone/mcp-server-microsoft-mail`](../microsoft-mail/) so the cohort has a single OAuth surface.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const COMPOSE_MESSAGE_HTML = "\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Compose message</title>\n <style>\n :root {\n --bg: #ffffff;\n --panel: #ffffff;\n --border: #d9dde5;\n --text: #1f2937;\n --muted: #6b7280;\n --input-bg: #ffffff;\n --button-bg: #1f6feb;\n --button-text: #ffffff;\n --button-bg-hover: #1b63d3;\n --secondary-bg: #f3f4f6;\n --secondary-text: #1f2937;\n --secondary-border: #d1d5db;\n --error-bg: #fef2f2;\n --error-border: #fecaca;\n --error-text: #991b1b;\n --success-bg: #ecfdf5;\n --success-border: #bbf7d0;\n --success-text: #166534;\n --warning-bg: #fffbeb;\n --warning-border: #fde68a;\n --warning-text: #92400e;\n }\n\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #0f172a;\n --panel: #111827;\n --border: #334155;\n --text: #e5e7eb;\n --muted: #9ca3af;\n --input-bg: #0b1220;\n --button-bg: #3b82f6;\n --button-text: #eff6ff;\n --button-bg-hover: #2563eb;\n --secondary-bg: #1f2937;\n --secondary-text: #e5e7eb;\n --secondary-border: #374151;\n --error-bg: #2b1011;\n --error-border: #7f1d1d;\n --error-text: #fca5a5;\n --success-bg: #052e1b;\n --success-border: #166534;\n --success-text: #86efac;\n --warning-bg: #2a2106;\n --warning-border: #854d0e;\n --warning-text: #fcd34d;\n }\n }\n\n html[data-theme=\"light\"] {\n color-scheme: light;\n }\n\n html[data-theme=\"dark\"] {\n color-scheme: dark;\n --bg: #0f172a;\n --panel: #111827;\n --border: #334155;\n --text: #e5e7eb;\n --muted: #9ca3af;\n --input-bg: #0b1220;\n --button-bg: #3b82f6;\n --button-text: #eff6ff;\n --button-bg-hover: #2563eb;\n --secondary-bg: #1f2937;\n --secondary-text: #e5e7eb;\n --secondary-border: #374151;\n --error-bg: #2b1011;\n --error-border: #7f1d1d;\n --error-text: #fca5a5;\n --success-bg: #052e1b;\n --success-border: #166534;\n --success-text: #86efac;\n --warning-bg: #2a2106;\n --warning-border: #854d0e;\n --warning-text: #fcd34d;\n }\n\n * {\n box-sizing: border-box;\n }\n\n body {\n margin: 0;\n padding: 16px;\n background: var(--bg);\n color: var(--text);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n line-height: 1.4;\n }\n\n .wrapper {\n width: 100%;\n max-width: 720px;\n margin: 0 auto;\n }\n\n .card {\n background: var(--panel);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 14px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n\n .field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .field label {\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n\n .from-value {\n color: var(--text);\n font-size: 14px;\n padding: 2px 0;\n word-break: break-word;\n }\n\n .from-helper {\n font-size: 12px;\n color: var(--muted);\n margin-top: 2px;\n }\n\n .input,\n .textarea {\n width: 100%;\n background: var(--input-bg);\n color: var(--text);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 10px 12px;\n font-size: 14px;\n font-family: inherit;\n }\n\n .input:focus,\n .textarea:focus {\n outline: 2px solid rgba(59, 130, 246, 0.35);\n outline-offset: 1px;\n }\n\n .textarea {\n min-height: 180px;\n resize: vertical;\n white-space: pre-wrap;\n }\n\n .toggles {\n display: flex;\n gap: 12px;\n align-items: center;\n flex-wrap: wrap;\n }\n\n .link-button {\n border: none;\n padding: 0;\n background: none;\n color: var(--muted);\n text-decoration: underline;\n font-size: 12px;\n cursor: pointer;\n }\n\n .link-button:hover {\n color: var(--text);\n }\n\n .actions {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n margin-top: 2px;\n }\n\n .button {\n border-radius: 8px;\n border: 1px solid transparent;\n min-height: 36px;\n padding: 0 14px;\n font-size: 13px;\n font-weight: 600;\n font-family: inherit;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: background 120ms ease, opacity 120ms ease, border-color 120ms ease;\n }\n\n .button:disabled {\n cursor: not-allowed;\n opacity: 0.75;\n }\n\n .button-primary {\n background: var(--button-bg);\n color: var(--button-text);\n }\n\n .button-primary:hover:enabled {\n background: var(--button-bg-hover);\n }\n\n .button-secondary {\n background: var(--secondary-bg);\n color: var(--secondary-text);\n border-color: var(--secondary-border);\n }\n\n .spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n display: inline-block;\n animation: spin 700ms linear infinite;\n }\n\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n\n .status {\n border-radius: 8px;\n border: 1px solid transparent;\n padding: 8px 10px;\n font-size: 13px;\n }\n\n .error {\n background: var(--error-bg);\n border-color: var(--error-border);\n color: var(--error-text);\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n }\n\n .success {\n background: var(--success-bg);\n border-color: var(--success-border);\n color: var(--success-text);\n }\n\n .warning {\n background: var(--warning-bg);\n border-color: var(--warning-border);\n color: var(--warning-text);\n display: block;\n }\n\n .warning strong {\n display: block;\n margin-bottom: 2px;\n }\n\n .warning .warning-detail {\n opacity: 0.85;\n }\n\n .collapsed {\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--panel);\n padding: 10px 12px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n }\n\n .hidden {\n display: none !important;\n }\n\n .sent-header {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n }\n\n .sent-badge {\n display: inline-flex;\n align-items: center;\n background: var(--success-bg);\n color: var(--success-text);\n border: 1px solid var(--success-border);\n border-radius: 999px;\n padding: 2px 10px;\n font-size: 12px;\n font-weight: 600;\n }\n\n .sent-timestamp {\n color: var(--muted);\n font-size: 12px;\n }\n\n .sent-value {\n color: var(--text);\n font-size: 14px;\n word-break: break-word;\n }\n\n .sent-body {\n color: var(--text);\n font-size: 14px;\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 260px;\n overflow: auto;\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 10px 12px;\n background: var(--input-bg);\n }\n </style>\n</head>\n<body>\n <div class=\"wrapper\">\n <div id=\"collapsedState\" class=\"collapsed hidden\">\n <span id=\"collapsedMessage\">Draft collapsed.</span>\n <button id=\"reopenButton\" type=\"button\" class=\"button button-secondary\">Reopen</button>\n </div>\n\n <form id=\"composeForm\" class=\"card\" novalidate aria-label=\"Compose message\">\n <div class=\"field\">\n <span id=\"workspaceLine\" class=\"from-helper hidden\"></span>\n </div>\n\n <div class=\"field\">\n <label for=\"toInput\">To</label>\n <input id=\"toInput\" class=\"input\" type=\"text\" autocomplete=\"off\" placeholder=\"Chat ID\">\n </div>\n\n <div class=\"field\">\n <label for=\"bodyInput\">Message</label>\n <textarea id=\"bodyInput\" class=\"textarea\" placeholder=\"Write your message\"></textarea>\n </div>\n\n <div id=\"errorBox\" class=\"status error hidden\" role=\"alert\">\n <span id=\"errorText\"></span>\n <button id=\"retryButton\" type=\"button\" class=\"link-button\">Retry</button>\n </div>\n\n <div id=\"successBox\" class=\"status success hidden\" role=\"status\" aria-live=\"polite\"></div>\n\n <div id=\"unknownBox\" class=\"status warning hidden\" role=\"status\" aria-live=\"polite\">\n <strong id=\"unknownTitle\"></strong>\n <span id=\"unknownText\" class=\"warning-detail\"></span>\n </div>\n\n <div class=\"actions\">\n <button id=\"cancelButton\" type=\"button\" class=\"button button-secondary\">Cancel</button>\n <button id=\"sendButton\" type=\"button\" class=\"button button-primary\">\n <span id=\"sendSpinner\" class=\"spinner hidden\" aria-hidden=\"true\"></span>\n <span id=\"sendLabel\">Send message</span>\n </button>\n </div>\n </form>\n\n <div id=\"sentView\" class=\"card hidden\" role=\"status\" aria-live=\"polite\">\n <div class=\"sent-header\">\n <span class=\"sent-badge\">Sent</span>\n <span id=\"sentTimestamp\" class=\"sent-timestamp\"></span>\n </div>\n\n <div class=\"field\">\n <label>To</label>\n <span id=\"sentTo\" class=\"sent-value\"></span>\n </div>\n\n <div class=\"field\">\n <label>Message</label>\n <div id=\"sentBody\" class=\"sent-body\"></div>\n </div>\n\n <div class=\"actions\">\n <button id=\"sentCollapseButton\" type=\"button\" class=\"button button-secondary\">Collapse</button>\n </div>\n </div>\n </div>\n\n <script>\n (function () {\n 'use strict';\n\n var RESOURCE_URI = 'ui://microsoft-teams/compose-message';\n var currentEmail = '';\n // The host prefills the draft exactly once via ui/notifications/tool-result\n // on ready. We apply only the FIRST prefill and ignore any later ones, so a\n // stray or hostile re-post can't silently rewrite a draft the user has\n // already reviewed before clicking Send.\n var draftApplied = false;\n var pendingRequestId = null;\n var pendingPayload = null;\n var sending = false;\n var collapsed = false;\n var outcomeUnknown = false;\n var sent = false;\n var sendTimeoutId = null;\n // Just beyond the host-side tool-call timeout (60s) so a legitimately slow\n // send resolves to its real result first; this only fires when the reply\n // is genuinely lost, so the button can never stay stuck-silent.\n var SEND_TIMEOUT_MS = 75000;\n\n var composeForm = document.getElementById('composeForm');\n var collapsedState = document.getElementById('collapsedState');\n var collapsedMessage = document.getElementById('collapsedMessage');\n var reopenButton = document.getElementById('reopenButton');\n\n var sentView = document.getElementById('sentView');\n var sentTimestamp = document.getElementById('sentTimestamp');\n var sentFrom = document.getElementById('sentFrom');\n var sentTo = document.getElementById('sentTo');\n var sentSubject = document.getElementById('sentSubject');\n var sentBody = document.getElementById('sentBody');\n var sentCollapseButton = document.getElementById('sentCollapseButton');\n\n var toInput = document.getElementById('toInput');\n var subjectInput = document.getElementById('subjectInput');\n var bodyInput = document.getElementById('bodyInput');\n var fromValue = document.getElementById('fromValue');\n var fromHelper = document.getElementById('fromHelper');\n\n var sendButton = document.getElementById('sendButton');\n var sendLabel = document.getElementById('sendLabel');\n var sendSpinner = document.getElementById('sendSpinner');\n var cancelButton = document.getElementById('cancelButton');\n var retryButton = document.getElementById('retryButton');\n\n var errorBox = document.getElementById('errorBox');\n var errorText = document.getElementById('errorText');\n var successBox = document.getElementById('successBox');\n var unknownBox = document.getElementById('unknownBox');\n var unknownTitle = document.getElementById('unknownTitle');\n var unknownText = document.getElementById('unknownText');\n\n function applyThemeFromHostContext() {\n try {\n var context = window.__MCP_HOST_CONTEXT__;\n if (!context || (context.theme !== 'light' && context.theme !== 'dark')) {\n return;\n }\n document.documentElement.setAttribute('data-theme', context.theme);\n } catch (_) {\n // Ignore theme extraction errors\n }\n }\n\n function trimString(value) {\n return String(value || '').trim();\n }\n\n function applyFromValue(email) {\n var trimmed = trimString(email);\n if (trimmed) {\n fromValue.textContent = trimmed;\n fromValue.style.color = '';\n fromHelper.textContent = '';\n fromHelper.classList.add('hidden');\n } else {\n fromValue.textContent = 'Account not shown';\n fromValue.style.color = 'var(--muted)';\n fromHelper.textContent = '';\n fromHelper.classList.remove('hidden');\n }\n }\n\n function formatAddressItem(item) {\n if (item && typeof item === 'object') {\n var email = trimString(item.email || item.address || item.value);\n var name = trimString(item.name || item.displayName);\n if (email && name) {\n return name + ' <' + email + '>';\n }\n return email;\n }\n return trimString(item);\n }\n\n function normalizeAddressList(value) {\n if (Array.isArray(value)) {\n return value.map(formatAddressItem).filter(Boolean);\n }\n if (typeof value === 'string') {\n return value\n .split(',')\n .map(function (item) { return trimString(item); })\n .filter(Boolean);\n }\n return [];\n }\n\n function listToInputValue(list) {\n return normalizeAddressList(list).join(', ');\n }\n\n var resizeQueued = false;\n function postResize() {\n if (resizeQueued) return;\n resizeQueued = true;\n requestAnimationFrame(function () {\n resizeQueued = false;\n var height = Math.max(\n document.documentElement.scrollHeight,\n document.body ? document.body.scrollHeight : 0\n );\n window.parent.postMessage(\n { jsonrpc: '2.0', method: 'ui/resize', params: { height: height } },\n '*'\n );\n });\n }\n\n function postReady() {\n window.parent.postMessage(\n {\n method: 'mcp-app:ready',\n params: { resourceUri: RESOURCE_URI }\n },\n '*'\n );\n }\n\n // Exactly one of the three panels is visible at a time: the editable form,\n // the read-only sent view, or the collapsed summary bar.\n function showView(view) {\n composeForm.classList.toggle('hidden', view !== 'form');\n sentView.classList.toggle('hidden', view !== 'sent');\n collapsedState.classList.toggle('hidden', view !== 'collapsed');\n postResize();\n }\n\n function setCollapsed(nextCollapsed, message) {\n collapsed = nextCollapsed;\n if (nextCollapsed) {\n collapsedMessage.textContent = message || 'Draft collapsed.';\n showView('collapsed');\n } else {\n // Expanding after a send restores the read-only sent view (never the\n // editable form) so the user can re-read what went out without any risk\n // of a duplicate send; before sending, it restores the editable form.\n showView(sent ? 'sent' : 'form');\n }\n }\n\n function setSending(nextSending) {\n sending = nextSending;\n sendButton.disabled = nextSending;\n cancelButton.disabled = nextSending;\n toInput.disabled = nextSending;\n bodyInput.disabled = nextSending;\n sendSpinner.classList.toggle('hidden', !nextSending);\n sendLabel.textContent = nextSending ? 'Sending\u2026' : 'Send message';\n sendButton.setAttribute('aria-busy', nextSending ? 'true' : 'false');\n postResize();\n }\n\n function clearError() {\n errorText.textContent = '';\n errorBox.classList.add('hidden');\n }\n\n function showError(message) {\n errorText.textContent = message || 'Failed to send message.';\n errorBox.classList.remove('hidden');\n postResize();\n }\n\n function clearSuccess() {\n successBox.textContent = '';\n successBox.classList.add('hidden');\n }\n\n function showSuccess(message) {\n successBox.textContent = message;\n successBox.classList.remove('hidden');\n postResize();\n }\n\n function clearSendTimeout() {\n if (sendTimeoutId !== null) {\n clearTimeout(sendTimeoutId);\n sendTimeoutId = null;\n }\n }\n\n function clearUnknown() {\n outcomeUnknown = false;\n unknownTitle.textContent = '';\n unknownText.textContent = '';\n unknownBox.classList.add('hidden');\n }\n\n // No reply ever came back for the send (e.g. the host's response was lost\n // before reaching this iframe). We genuinely don't know whether the email\n // went out, so we must NOT claim success or failure: re-enable the form and\n // tell the user to verify before resending (the send is not idempotent, so\n // a blind retry could send a duplicate).\n function showSendUnknown() {\n outcomeUnknown = true;\n clearError();\n clearSuccess();\n unknownTitle.textContent = 'Not sure if this sent.';\n unknownText.textContent = \"Rebel didn't hear back in time. Check whether it sent before sending again, so it doesn't go out twice.\";\n unknownBox.classList.remove('hidden');\n postResize();\n }\n\n function handleSendTimeout() {\n sendTimeoutId = null;\n if (!pendingRequestId) {\n return;\n }\n pendingRequestId = null;\n setSending(false);\n showSendUnknown();\n }\n\n function readFormPayload() {\n var chatId = String(toInput.value || '').trim();\n var payload = {\n chatId: chatId,\n content: String(bodyInput.value || '')\n };\n return payload;\n }\n\n function validatePayload(payload) {\n if (!trimString(payload.chatId)) {\n return 'Add a chat in To.';\n }\n if (!trimString(payload.content)) {\n return 'Add a message before sending.';\n }\n return null;\n }\n\n function sendPayload(payload) {\n pendingPayload = payload;\n pendingRequestId = 'send-' + Date.now() + '-' + Math.random().toString(16).slice(2);\n setSending(true);\n clearError();\n clearSuccess();\n clearUnknown();\n clearSendTimeout();\n sendTimeoutId = setTimeout(handleSendTimeout, SEND_TIMEOUT_MS);\n\n window.parent.postMessage(\n {\n jsonrpc: '2.0',\n method: 'tools/call',\n params: {\n name: 'send_chat_message',\n arguments: payload\n },\n id: pendingRequestId\n },\n '*'\n );\n }\n\n function applyDraftData(rawDraft) {\n var draft = rawDraft && typeof rawDraft === 'object' ? rawDraft : {};\n toInput.value = String(draft.target || '');\n bodyInput.value = String(draft.text || '');\n clearError();\n clearSuccess();\n postResize();\n }\n\n function getDraftFromToolResult(payload) {\n if (!payload || typeof payload !== 'object') {\n return null;\n }\n // CANONICAL: post-A0 super-mcp hoists structuredContent onto the\n // outer block; the host posts it through `params.structuredContent`.\n if (payload.structuredContent && typeof payload.structuredContent === 'object') {\n return payload.structuredContent;\n }\n // MIGRATION SHIM: pre-A0 stored sessions replayed through the\n // current iframe lack `params.structuredContent` because the outer\n // block had no hoist at capture time. Read inner envelope text \u2192\n // JSON.parse \u2192 result.structuredContent. NOT regex-based.\n //\n // Lifetime: PERMANENT defensive read-side helper. Pre-A0 sessions\n // are persisted on disk and may be replayed indefinitely; there is\n // no time-based sunset. Removing this fallback would silently break\n // pre-A0 conversation replay.\n var text = typeof payload.text === 'string' ? payload.text :\n (Array.isArray(payload.content)\n ? (payload.content.find(function (c) { return c && c.type === 'text' && typeof c.text === 'string'; }) || {}).text\n : null);\n if (typeof text !== 'string') return null;\n try {\n var parsed = JSON.parse(text);\n if (parsed && typeof parsed === 'object' && parsed.result && typeof parsed.result === 'object') {\n var inner = parsed.result;\n if (inner.structuredContent && typeof inner.structuredContent === 'object') {\n return inner.structuredContent;\n }\n }\n } catch (_) {\n // Likely-envelope heuristic: text starts with `{` and contains\n // `\"package_id\"` in the first 64 chars. If JSON.parse failed on a\n // likely-envelope, surface a warning so the developer chasing\n // \"compose form is empty on replay\" sees a signal. For non-envelope\n // text (the dominant case), no log fires.\n var prefix = text.length > 64 ? text.slice(0, 64) : text;\n var likelyEnvelope = prefix.charAt(0) === '{' && prefix.indexOf('\"package_id\"') !== -1;\n if (likelyEnvelope && typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn('[compose-email] Migration shim: JSON.parse failed on likely super-mcp envelope; pre-fill skipped');\n }\n }\n return null;\n }\n\n function findTextBlock(blocks) {\n if (!Array.isArray(blocks)) return null;\n var block = blocks.find(function (c) {\n return c && c.type === 'text' && typeof c.text === 'string';\n });\n return block ? block.text : null;\n }\n\n // Extract the send result identifiers ({ messageId, threadId, labelIds })\n // from the tool-call reply. The connector attaches structuredContent, which\n // super-mcp hoists onto the outer block, so the fast path is a single read.\n // The envelope-unwrapping fallback keeps this robust for hosts (or replayed\n // sessions) that forward only the serialized super-mcp text envelope.\n function getSendMetaFromResult(result) {\n if (!result || typeof result !== 'object') return null;\n if (result.structuredContent && typeof result.structuredContent === 'object') {\n return result.structuredContent;\n }\n try {\n var outerText = findTextBlock(result.content);\n if (typeof outerText !== 'string' && typeof result.text === 'string') {\n outerText = result.text;\n }\n if (typeof outerText !== 'string') return null;\n var envelope = JSON.parse(outerText);\n if (!envelope || typeof envelope !== 'object') return null;\n // outerText was the connector payload directly (no super-mcp envelope).\n if (envelope.messageId || envelope.threadId) return envelope;\n var inner = envelope.result;\n if (!inner || typeof inner !== 'object') return null;\n if (inner.structuredContent && typeof inner.structuredContent === 'object') {\n return inner.structuredContent;\n }\n var innerText = findTextBlock(inner.content);\n if (typeof innerText !== 'string') return null;\n var payload = JSON.parse(innerText);\n return (payload && typeof payload === 'object') ? payload : null;\n } catch (_) {\n return null;\n }\n }\n\n function formatSentTime() {\n try {\n return new Date().toLocaleString(undefined, {\n month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit'\n });\n } catch (_) {\n return '';\n }\n }\n\n function renderSentView(payload, meta, stamp) {\n payload = payload && typeof payload === 'object' ? payload : {};\n sentTimestamp.textContent = stamp ? 'Sent ' + stamp : 'Sent';\n sentTo.textContent = String(payload.channel || payload.chatId || '');\n sentBody.textContent = String(payload.text || payload.content || '');\n }\n\n function handleSendRequest() {\n if (sending) return;\n\n var payload = readFormPayload();\n var validationError = validatePayload(payload);\n if (validationError) {\n showError(validationError);\n return;\n }\n\n sendPayload(payload);\n }\n\n // The Send button is type=\"button\" with an explicit click handler rather\n // than a form submit button. The Rebel host renders this iframe with a\n // sandbox of \"allow-scripts\" only \u2014 no \"allow-forms\" \u2014 so native form\n // submission is blocked by the browser BEFORE the submit event fires\n // (\"Blocked form submission ... the form's frame is sandboxed and the\n // allow-forms permission is not set\"). A submit-driven Send therefore\n // silently did nothing: no postMessage, no spinner, no telemetry.\n // A click handler needs no sandbox permission and is the same path\n // Cancel/Retry already use. The submit listener below stays only as a\n // defensive fallback for hosts that DO grant allow-forms; in this sandbox\n // it never fires (neither the button click nor Enter routes through it).\n // The sending guard keeps any double-fire (in an allow-forms host) a no-op.\n sendButton.addEventListener('click', function () {\n handleSendRequest();\n });\n\n composeForm.addEventListener('submit', function (event) {\n event.preventDefault();\n handleSendRequest();\n });\n\n retryButton.addEventListener('click', function () {\n if (sending) return;\n var payload = readFormPayload();\n var validationError = validatePayload(payload);\n if (validationError) {\n showError(validationError);\n return;\n }\n sendPayload(payload);\n });\n\n cancelButton.addEventListener('click', function () {\n if (sending) return;\n // After a lost-reply timeout we don't know if the email sent, so don't\n // claim \"Nothing sent.\" on collapse \u2014 point the user to verify instead.\n var collapseMessage = outcomeUnknown\n ? 'Collapsed. Check whether it sent if you are not sure.'\n : 'Draft collapsed. Nothing sent.';\n clearError();\n clearSuccess();\n clearUnknown();\n setCollapsed(true, collapseMessage);\n });\n\n reopenButton.addEventListener('click', function () {\n setCollapsed(false);\n });\n\n sentCollapseButton.addEventListener('click', function () {\n setCollapsed(true, collapsedMessage.textContent || 'Message sent.');\n });\n\n [toInput, bodyInput].forEach(function (element) {\n element.addEventListener('input', function () {\n clearError();\n clearSuccess();\n clearUnknown();\n postResize();\n });\n });\n\n window.addEventListener('message', function (event) {\n // Honest scope (origin-independent): the iframe sandbox origin is 'null',\n // so validating event.origin is unreliable across dev/prod/custom-scheme\n // hosts. Instead reject any *identified* sender that isn't our host frame\n // (window.parent). A real cross-frame postMessage always stamps\n // event.source with the sending window, so a hostile sibling frame is\n // rejected here; a null source only occurs for same-context synthetic\n // dispatch (our own code) and cannot be forged from outside, so we\n // tolerate it. This does not authenticate the host's identity \u2014 the host\n // separately authenticates this iframe by origin+source.\n if (event.source && event.source !== window.parent) {\n return;\n }\n\n var data = event.data;\n if (!data || typeof data !== 'object') {\n return;\n }\n\n if (data.method === 'ui/notifications/tool-result') {\n // Always parse (so the migration-shim's malformed-envelope warning\n // still fires), but apply only the first successful prefill: a stray\n // or hostile re-post can't silently rewrite a draft the user has\n // already reviewed.\n var draft = getDraftFromToolResult(data.params);\n if (draft && !draftApplied) {\n draftApplied = true;\n applyDraftData(draft);\n }\n return;\n }\n\n if (data.jsonrpc !== '2.0' || !pendingRequestId || data.id !== pendingRequestId) {\n return;\n }\n\n pendingRequestId = null;\n clearSendTimeout();\n clearUnknown();\n setSending(false);\n\n if (data.error) {\n var errorMessage = data.error && typeof data.error.message === 'string'\n ? data.error.message\n : 'Failed to send message.';\n showError(errorMessage);\n return;\n }\n\n clearError();\n clearSuccess();\n // Preserve what was sent instead of discarding the draft. pendingPayload\n // is the exact payload we posted. We default to the collapsed summary, but\n // Reopen now restores a read-only view of the sent message.\n var sentPayload = pendingPayload || readFormPayload();\n var stamp = formatSentTime();\n renderSentView(sentPayload, getSendMetaFromResult(data.result), stamp);\n sent = true;\n setCollapsed(true, stamp ? 'Message sent \u00B7 ' + stamp : 'Message sent.');\n });\n\n if (typeof ResizeObserver === 'function' && document.body) {\n var observer = new ResizeObserver(postResize);\n observer.observe(document.body);\n }\n\n applyThemeFromHostContext();\n setCollapsed(false);\n postResize();\n postReady();\n\n window.parent.postMessage(\n {\n jsonrpc: '2.0',\n method: 'ui/initialize',\n params: { resourceUri: RESOURCE_URI },\n id: 'compose-email-init'\n },\n '*'\n );\n })();\n </script>\n</body>\n</html>\n";
|
|
1
|
+
export declare const COMPOSE_MESSAGE_HTML = "\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>Compose message</title>\n <style>\n :root {\n --bg: #ffffff;\n --panel: #ffffff;\n --border: #d9dde5;\n --text: #1f2937;\n --muted: #6b7280;\n --input-bg: #ffffff;\n --button-bg: #1f6feb;\n --button-text: #ffffff;\n --button-bg-hover: #1b63d3;\n --secondary-bg: #f3f4f6;\n --secondary-text: #1f2937;\n --secondary-border: #d1d5db;\n --error-bg: #fef2f2;\n --error-border: #fecaca;\n --error-text: #991b1b;\n --success-bg: #ecfdf5;\n --success-border: #bbf7d0;\n --success-text: #166534;\n --warning-bg: #fffbeb;\n --warning-border: #fde68a;\n --warning-text: #92400e;\n }\n\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #0f172a;\n --panel: #111827;\n --border: #334155;\n --text: #e5e7eb;\n --muted: #9ca3af;\n --input-bg: #0b1220;\n --button-bg: #3b82f6;\n --button-text: #eff6ff;\n --button-bg-hover: #2563eb;\n --secondary-bg: #1f2937;\n --secondary-text: #e5e7eb;\n --secondary-border: #374151;\n --error-bg: #2b1011;\n --error-border: #7f1d1d;\n --error-text: #fca5a5;\n --success-bg: #052e1b;\n --success-border: #166534;\n --success-text: #86efac;\n --warning-bg: #2a2106;\n --warning-border: #854d0e;\n --warning-text: #fcd34d;\n }\n }\n\n html[data-theme=\"light\"] {\n color-scheme: light;\n }\n\n html[data-theme=\"dark\"] {\n color-scheme: dark;\n --bg: #0f172a;\n --panel: #111827;\n --border: #334155;\n --text: #e5e7eb;\n --muted: #9ca3af;\n --input-bg: #0b1220;\n --button-bg: #3b82f6;\n --button-text: #eff6ff;\n --button-bg-hover: #2563eb;\n --secondary-bg: #1f2937;\n --secondary-text: #e5e7eb;\n --secondary-border: #374151;\n --error-bg: #2b1011;\n --error-border: #7f1d1d;\n --error-text: #fca5a5;\n --success-bg: #052e1b;\n --success-border: #166534;\n --success-text: #86efac;\n --warning-bg: #2a2106;\n --warning-border: #854d0e;\n --warning-text: #fcd34d;\n }\n\n * {\n box-sizing: border-box;\n }\n\n body {\n margin: 0;\n padding: 16px;\n background: var(--bg);\n color: var(--text);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n line-height: 1.4;\n }\n\n .wrapper {\n width: 100%;\n max-width: 720px;\n margin: 0 auto;\n }\n\n .card {\n background: var(--panel);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 14px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n\n .field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .field label {\n font-size: 12px;\n font-weight: 600;\n color: var(--muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n\n .from-value {\n color: var(--text);\n font-size: 14px;\n padding: 2px 0;\n word-break: break-word;\n }\n\n .from-helper {\n font-size: 12px;\n color: var(--muted);\n margin-top: 2px;\n }\n\n .input,\n .textarea {\n width: 100%;\n background: var(--input-bg);\n color: var(--text);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 10px 12px;\n font-size: 14px;\n font-family: inherit;\n }\n\n .input:focus,\n .textarea:focus {\n outline: 2px solid rgba(59, 130, 246, 0.35);\n outline-offset: 1px;\n }\n\n .textarea {\n min-height: 180px;\n resize: vertical;\n white-space: pre-wrap;\n }\n\n .toggles {\n display: flex;\n gap: 12px;\n align-items: center;\n flex-wrap: wrap;\n }\n\n .link-button {\n border: none;\n padding: 0;\n background: none;\n color: var(--muted);\n text-decoration: underline;\n font-size: 12px;\n cursor: pointer;\n }\n\n .link-button:hover {\n color: var(--text);\n }\n\n .actions {\n display: flex;\n justify-content: flex-end;\n gap: 10px;\n margin-top: 2px;\n }\n\n .button {\n border-radius: 8px;\n border: 1px solid transparent;\n min-height: 36px;\n padding: 0 14px;\n font-size: 13px;\n font-weight: 600;\n font-family: inherit;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: background 120ms ease, opacity 120ms ease, border-color 120ms ease;\n }\n\n .button:disabled {\n cursor: not-allowed;\n opacity: 0.75;\n }\n\n .button-primary {\n background: var(--button-bg);\n color: var(--button-text);\n }\n\n .button-primary:hover:enabled {\n background: var(--button-bg-hover);\n }\n\n .button-secondary {\n background: var(--secondary-bg);\n color: var(--secondary-text);\n border-color: var(--secondary-border);\n }\n\n .spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n display: inline-block;\n animation: spin 700ms linear infinite;\n }\n\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n\n .status {\n border-radius: 8px;\n border: 1px solid transparent;\n padding: 8px 10px;\n font-size: 13px;\n }\n\n .error {\n background: var(--error-bg);\n border-color: var(--error-border);\n color: var(--error-text);\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n }\n\n .success {\n background: var(--success-bg);\n border-color: var(--success-border);\n color: var(--success-text);\n }\n\n .warning {\n background: var(--warning-bg);\n border-color: var(--warning-border);\n color: var(--warning-text);\n display: block;\n }\n\n .warning strong {\n display: block;\n margin-bottom: 2px;\n }\n\n .warning .warning-detail {\n opacity: 0.85;\n }\n\n .collapsed {\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--panel);\n padding: 10px 12px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n }\n\n .hidden {\n display: none !important;\n }\n\n .sent-header {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n }\n\n .sent-badge {\n display: inline-flex;\n align-items: center;\n background: var(--success-bg);\n color: var(--success-text);\n border: 1px solid var(--success-border);\n border-radius: 999px;\n padding: 2px 10px;\n font-size: 12px;\n font-weight: 600;\n }\n\n .sent-timestamp {\n color: var(--muted);\n font-size: 12px;\n }\n\n .sent-value {\n color: var(--text);\n font-size: 14px;\n word-break: break-word;\n }\n\n .sent-body {\n color: var(--text);\n font-size: 14px;\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 260px;\n overflow: auto;\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 10px 12px;\n background: var(--input-bg);\n }\n </style>\n</head>\n<body>\n <div class=\"wrapper\">\n <div id=\"collapsedState\" class=\"collapsed hidden\">\n <span id=\"collapsedMessage\">Draft collapsed.</span>\n <button id=\"reopenButton\" type=\"button\" class=\"button button-secondary\">Reopen</button>\n </div>\n\n <form id=\"composeForm\" class=\"card\" novalidate aria-label=\"Compose message\">\n <div class=\"field\">\n <span id=\"workspaceLine\" class=\"from-helper hidden\"></span>\n </div>\n\n <div class=\"field\">\n <label for=\"toInput\">To</label>\n <input id=\"toInput\" class=\"input\" type=\"text\" autocomplete=\"off\" placeholder=\"Chat ID\">\n </div>\n\n <div class=\"field\">\n <label for=\"bodyInput\">Message</label>\n <textarea id=\"bodyInput\" class=\"textarea\" placeholder=\"Write your message\"></textarea>\n </div>\n\n <div id=\"errorBox\" class=\"status error hidden\" role=\"alert\">\n <span id=\"errorText\"></span>\n <button id=\"retryButton\" type=\"button\" class=\"link-button\">Retry</button>\n </div>\n\n <div id=\"successBox\" class=\"status success hidden\" role=\"status\" aria-live=\"polite\"></div>\n\n <div id=\"unknownBox\" class=\"status warning hidden\" role=\"status\" aria-live=\"polite\">\n <strong id=\"unknownTitle\"></strong>\n <span id=\"unknownText\" class=\"warning-detail\"></span>\n </div>\n\n <div class=\"actions\">\n <button id=\"cancelButton\" type=\"button\" class=\"button button-secondary\">Cancel</button>\n <button id=\"sendButton\" type=\"button\" class=\"button button-primary\">\n <span id=\"sendSpinner\" class=\"spinner hidden\" aria-hidden=\"true\"></span>\n <span id=\"sendLabel\">Send message</span>\n </button>\n </div>\n </form>\n\n <div id=\"sentView\" class=\"card hidden\" role=\"status\" aria-live=\"polite\">\n <div class=\"sent-header\">\n <span class=\"sent-badge\">Sent</span>\n <span id=\"sentTimestamp\" class=\"sent-timestamp\"></span>\n </div>\n\n <div class=\"field\">\n <label>To</label>\n <span id=\"sentTo\" class=\"sent-value\"></span>\n </div>\n\n <div class=\"field\">\n <label>Message</label>\n <div id=\"sentBody\" class=\"sent-body\"></div>\n </div>\n\n <div class=\"actions\">\n <button id=\"sentCollapseButton\" type=\"button\" class=\"button button-secondary\">Collapse</button>\n </div>\n </div>\n </div>\n\n <script>\n (function () {\n 'use strict';\n\n var RESOURCE_URI = 'ui://microsoft-teams/compose-message';\n var currentEmail = '';\n // The host prefills the draft exactly once via ui/notifications/tool-result\n // on ready. We apply only the FIRST prefill and ignore any later ones, so a\n // stray or hostile re-post can't silently rewrite a draft the user has\n // already reviewed before clicking Send.\n var draftApplied = false;\n var pendingRequestId = null;\n var pendingSendPayload = null;\n // Retain across the local timeout so even a late reply clears its payload.\n var pendingSendRequestId = null;\n var sending = false;\n var hostSending = false;\n var collapsed = false;\n var sendMayHaveGone = false;\n var sent = false;\n var sendBlocked = false;\n var sendTimeoutId = null;\n // Just beyond the host-side tool-call timeout (60s) so a legitimately slow\n // send resolves to its real result first; this only fires when the reply\n // is genuinely lost, so the button can never stay stuck-silent.\n var SEND_TIMEOUT_MS = 75000;\n\n var composeForm = document.getElementById('composeForm');\n var collapsedState = document.getElementById('collapsedState');\n var collapsedMessage = document.getElementById('collapsedMessage');\n var reopenButton = document.getElementById('reopenButton');\n\n var sentView = document.getElementById('sentView');\n var sentTimestamp = document.getElementById('sentTimestamp');\n var sentFrom = document.getElementById('sentFrom');\n var sentTo = document.getElementById('sentTo');\n var sentSubject = document.getElementById('sentSubject');\n var sentBody = document.getElementById('sentBody');\n var sentCollapseButton = document.getElementById('sentCollapseButton');\n\n var toInput = document.getElementById('toInput');\n var subjectInput = document.getElementById('subjectInput');\n var bodyInput = document.getElementById('bodyInput');\n var fromValue = document.getElementById('fromValue');\n var fromHelper = document.getElementById('fromHelper');\n\n var sendButton = document.getElementById('sendButton');\n var sendLabel = document.getElementById('sendLabel');\n var sendSpinner = document.getElementById('sendSpinner');\n var cancelButton = document.getElementById('cancelButton');\n var retryButton = document.getElementById('retryButton');\n\n var errorBox = document.getElementById('errorBox');\n var errorText = document.getElementById('errorText');\n var successBox = document.getElementById('successBox');\n var unknownBox = document.getElementById('unknownBox');\n var unknownTitle = document.getElementById('unknownTitle');\n var unknownText = document.getElementById('unknownText');\n\n function applyThemeFromHostContext() {\n try {\n var context = window.__MCP_HOST_CONTEXT__;\n if (!context || (context.theme !== 'light' && context.theme !== 'dark')) {\n return;\n }\n document.documentElement.setAttribute('data-theme', context.theme);\n } catch (_) {\n // Ignore theme extraction errors\n }\n }\n\n function trimString(value) {\n return String(value || '').trim();\n }\n\n function applyFromValue(email) {\n var trimmed = trimString(email);\n if (trimmed) {\n fromValue.textContent = trimmed;\n fromValue.style.color = '';\n fromHelper.textContent = '';\n fromHelper.classList.add('hidden');\n } else {\n fromValue.textContent = 'Account not shown';\n fromValue.style.color = 'var(--muted)';\n fromHelper.textContent = '';\n fromHelper.classList.remove('hidden');\n }\n }\n\n function formatAddressItem(item) {\n if (item && typeof item === 'object') {\n var email = trimString(item.email || item.address || item.value);\n var name = trimString(item.name || item.displayName);\n if (email && name) {\n return name + ' <' + email + '>';\n }\n return email;\n }\n return trimString(item);\n }\n\n function normalizeAddressList(value) {\n if (Array.isArray(value)) {\n return value.map(formatAddressItem).filter(Boolean);\n }\n if (typeof value === 'string') {\n return value\n .split(',')\n .map(function (item) { return trimString(item); })\n .filter(Boolean);\n }\n return [];\n }\n\n function listToInputValue(list) {\n return normalizeAddressList(list).join(', ');\n }\n\n var resizeQueued = false;\n function postResize() {\n if (resizeQueued) return;\n resizeQueued = true;\n requestAnimationFrame(function () {\n resizeQueued = false;\n var height = Math.max(\n document.documentElement.scrollHeight,\n document.body ? document.body.scrollHeight : 0\n );\n window.parent.postMessage(\n { jsonrpc: '2.0', method: 'ui/resize', params: { height: height } },\n '*'\n );\n });\n }\n\n function postReady() {\n window.parent.postMessage(\n {\n method: 'mcp-app:ready',\n params: { resourceUri: RESOURCE_URI }\n },\n '*'\n );\n }\n\n // Exactly one of the three panels is visible at a time: the editable form,\n // the read-only sent view, or the collapsed summary bar.\n function showView(view) {\n composeForm.classList.toggle('hidden', view !== 'form');\n sentView.classList.toggle('hidden', view !== 'sent');\n collapsedState.classList.toggle('hidden', view !== 'collapsed');\n postResize();\n }\n\n function setCollapsed(nextCollapsed, message) {\n collapsed = nextCollapsed;\n if (nextCollapsed) {\n collapsedMessage.textContent = message || 'Draft collapsed.';\n showView('collapsed');\n } else {\n // Expanding after a send restores the read-only sent view (never the\n // editable form) so the user can re-read what went out without any risk\n // of a duplicate send; before sending, it restores the editable form.\n showView(sent ? 'sent' : 'form');\n }\n }\n\n function setSending(nextSending) {\n // Only a host outcome can release another copy's in-flight send.\n nextSending = nextSending || hostSending;\n sending = nextSending;\n sendButton.disabled = nextSending || sendBlocked;\n retryButton.disabled = nextSending;\n cancelButton.disabled = nextSending;\n toInput.disabled = nextSending;\n bodyInput.disabled = nextSending;\n sendSpinner.classList.toggle('hidden', !nextSending);\n sendLabel.textContent = nextSending ? 'Sending\u2026' : 'Send message';\n sendButton.setAttribute('aria-busy', nextSending ? 'true' : 'false');\n postResize();\n }\n\n function clearError() {\n errorText.textContent = '';\n errorBox.classList.add('hidden');\n }\n\n function showError(message) {\n retryButton.classList.toggle('hidden', sendBlocked);\n errorText.textContent = message || 'Failed to send message.';\n errorBox.classList.remove('hidden');\n postResize();\n }\n\n function clearSuccess() {\n successBox.textContent = '';\n successBox.classList.add('hidden');\n }\n\n function showSuccess(message) {\n successBox.textContent = message;\n successBox.classList.remove('hidden');\n postResize();\n }\n\n function clearSendTimeout() {\n if (sendTimeoutId !== null) {\n clearTimeout(sendTimeoutId);\n sendTimeoutId = null;\n }\n }\n\n function clearUnknown() {\n unknownTitle.textContent = '';\n unknownText.textContent = '';\n unknownBox.classList.add('hidden');\n errorBox.appendChild(retryButton);\n }\n\n // No reply ever came back for the send (e.g. the host's response was lost\n // before reaching this iframe). We genuinely don't know whether the email\n // went out, so we must NOT claim success or failure: re-enable the form and\n // tell the user to verify before resending (the send is not idempotent, so\n // a blind retry could send a duplicate).\n function showSendUnknown() {\n sendMayHaveGone = true;\n clearError();\n clearSuccess();\n unknownTitle.textContent = 'Not sure if this sent.';\n unknownText.textContent = \"No confirmation came back in time. Check whether it sent before sending again, so it doesn't go out twice.\";\n unknownBox.classList.remove('hidden');\n if (!sendBlocked) unknownBox.appendChild(retryButton);\n postResize();\n }\n\n function handleSendTimeout() {\n sendTimeoutId = null;\n if (!pendingRequestId || sent) {\n return;\n }\n pendingRequestId = null;\n setSending(false);\n showSendUnknown();\n }\n\n function readFormPayload() {\n var chatId = String(toInput.value || '').trim();\n var payload = {\n chatId: chatId,\n content: String(bodyInput.value || '')\n };\n return payload;\n }\n\n function validatePayload(payload) {\n if (!trimString(payload.chatId)) {\n return 'Add a chat in To.';\n }\n if (!trimString(payload.content)) {\n return 'Add a message before sending.';\n }\n return null;\n }\n\n function sendPayload(payload) {\n pendingSendPayload = payload;\n pendingRequestId = 'send-' + Date.now() + '-' + Math.random().toString(16).slice(2);\n pendingSendRequestId = pendingRequestId;\n setSending(true);\n clearError();\n clearSuccess();\n clearUnknown();\n clearSendTimeout();\n sendTimeoutId = setTimeout(handleSendTimeout, SEND_TIMEOUT_MS);\n\n window.parent.postMessage(\n {\n jsonrpc: '2.0',\n method: 'tools/call',\n params: {\n name: 'send_chat_message',\n _meta: { 'mcp-app/cardOutcome': 'sent' },\n arguments: payload\n },\n id: pendingRequestId\n },\n '*'\n );\n }\n\n function applyDraftData(rawDraft) {\n var draft = rawDraft && typeof rawDraft === 'object' ? rawDraft : {};\n toInput.value = String(draft.target || '');\n bodyInput.value = String(draft.text || '');\n clearError();\n clearSuccess();\n postResize();\n }\n\n function getDraftFromToolResult(payload) {\n if (!payload || typeof payload !== 'object') {\n return null;\n }\n // CANONICAL: post-A0 super-mcp hoists structuredContent onto the\n // outer block; the host posts it through `params.structuredContent`.\n if (payload.structuredContent && typeof payload.structuredContent === 'object') {\n return payload.structuredContent;\n }\n // MIGRATION SHIM: pre-A0 stored sessions replayed through the\n // current iframe lack `params.structuredContent` because the outer\n // block had no hoist at capture time. Read inner envelope text \u2192\n // JSON.parse \u2192 result.structuredContent. NOT regex-based.\n //\n // Lifetime: PERMANENT defensive read-side helper. Pre-A0 sessions\n // are persisted on disk and may be replayed indefinitely; there is\n // no time-based sunset. Removing this fallback would silently break\n // pre-A0 conversation replay.\n var text = typeof payload.text === 'string' ? payload.text :\n (Array.isArray(payload.content)\n ? (payload.content.find(function (c) { return c && c.type === 'text' && typeof c.text === 'string'; }) || {}).text\n : null);\n if (typeof text !== 'string') return null;\n try {\n var parsed = JSON.parse(text);\n if (parsed && typeof parsed === 'object' && parsed.result && typeof parsed.result === 'object') {\n var inner = parsed.result;\n if (inner.structuredContent && typeof inner.structuredContent === 'object') {\n return inner.structuredContent;\n }\n }\n } catch (_) {\n // Likely-envelope heuristic: text starts with `{` and contains\n // `\"package_id\"` in the first 64 chars. If JSON.parse failed on a\n // likely-envelope, surface a warning so the developer chasing\n // \"compose form is empty on replay\" sees a signal. For non-envelope\n // text (the dominant case), no log fires.\n var prefix = text.length > 64 ? text.slice(0, 64) : text;\n var likelyEnvelope = prefix.charAt(0) === '{' && prefix.indexOf('\"package_id\"') !== -1;\n if (likelyEnvelope && typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn('[compose-email] Migration shim: JSON.parse failed on likely super-mcp envelope; pre-fill skipped');\n }\n }\n return null;\n }\n\n function findTextBlock(blocks) {\n if (!Array.isArray(blocks)) return null;\n var block = blocks.find(function (c) {\n return c && c.type === 'text' && typeof c.text === 'string';\n });\n return block ? block.text : null;\n }\n\n // Extract the send result identifiers ({ messageId, threadId, labelIds })\n // from the tool-call reply. The connector attaches structuredContent, which\n // super-mcp hoists onto the outer block, so the fast path is a single read.\n // The envelope-unwrapping fallback keeps this robust for hosts (or replayed\n // sessions) that forward only the serialized super-mcp text envelope.\n function getSendMetaFromResult(result) {\n if (!result || typeof result !== 'object') return null;\n if (result.structuredContent && typeof result.structuredContent === 'object') {\n return result.structuredContent;\n }\n try {\n var outerText = findTextBlock(result.content);\n if (typeof outerText !== 'string' && typeof result.text === 'string') {\n outerText = result.text;\n }\n if (typeof outerText !== 'string') return null;\n var envelope = JSON.parse(outerText);\n if (!envelope || typeof envelope !== 'object') return null;\n // outerText was the connector payload directly (no super-mcp envelope).\n if (envelope.messageId || envelope.threadId) return envelope;\n var inner = envelope.result;\n if (!inner || typeof inner !== 'object') return null;\n if (inner.structuredContent && typeof inner.structuredContent === 'object') {\n return inner.structuredContent;\n }\n var innerText = findTextBlock(inner.content);\n if (typeof innerText !== 'string') return null;\n var payload = JSON.parse(innerText);\n return (payload && typeof payload === 'object') ? payload : null;\n } catch (_) {\n return null;\n }\n }\n\n function formatSentTime(at) {\n if (typeof at !== 'number' || !Number.isFinite(at)) return '';\n var date = new Date(at);\n if (!Number.isFinite(date.getTime())) return '';\n try {\n return date.toLocaleString(undefined, {\n month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit'\n });\n } catch (_) {\n return '';\n }\n }\n\n function applyCardOutcome(o) {\n if (sent) return;\n if (pendingRequestId && pendingRequestId === pendingSendRequestId &&\n (!o || o.outcome === 'sending' || o.outcome === 'maybe-sent')) return;\n if (hostSending) {\n hostSending = false;\n setSending(false);\n if (sendMayHaveGone) showSendUnknown();\n }\n if (!o || typeof o !== 'object') return;\n if (o.outcome === 'sending') {\n hostSending = true;\n setCollapsed(false);\n setSending(true);\n clearUnknown();\n return;\n }\n if (o.outcome === 'maybe-sent') {\n showSendUnknown();\n setCollapsed(false);\n return;\n }\n if (o.outcome !== 'sent' && o.outcome !== 'submitted') return;\n sent = true;\n sendMayHaveGone = false;\n clearSendTimeout();\n clearError();\n clearSuccess();\n clearUnknown();\n var stamp = formatSentTime(o.at);\n setCollapsed(true, stamp ? 'Message sent \u00B7 ' + stamp : 'Message sent.');\n if (pendingSendPayload) renderSentView(pendingSendPayload, null, stamp);\n reopenButton.classList.toggle('hidden', !pendingSendPayload);\n }\n\n function renderSentView(payload, meta, stamp) {\n payload = payload && typeof payload === 'object' ? payload : {};\n sentTimestamp.textContent = stamp ? 'Sent ' + stamp : 'Sent';\n sentTo.textContent = String(payload.channel || payload.chatId || '');\n sentBody.textContent = String(payload.text || payload.content || '');\n }\n\n function handleSendRequest() {\n if (sent) return;\n if (sending) return;\n if (sendBlocked) return;\n var payload = readFormPayload();\n var validationError = validatePayload(payload);\n if (validationError) {\n showError(validationError);\n return;\n }\n\n sendPayload(payload);\n }\n\n // The Send button is type=\"button\" with an explicit click handler rather\n // than a form submit button. The host renders this iframe with a\n // sandbox of \"allow-scripts\" only \u2014 no \"allow-forms\" \u2014 so native form\n // submission is blocked by the browser BEFORE the submit event fires\n // (\"Blocked form submission ... the form's frame is sandboxed and the\n // allow-forms permission is not set\"). A submit-driven Send therefore\n // silently did nothing: no postMessage, no spinner, no telemetry.\n // A click handler needs no sandbox permission and is the same path\n // Cancel/Retry already use. The submit listener below stays only as a\n // defensive fallback for hosts that DO grant allow-forms; in this sandbox\n // it never fires (neither the button click nor Enter routes through it).\n // The sending guard keeps any double-fire (in an allow-forms host) a no-op.\n sendButton.addEventListener('click', function () {\n handleSendRequest();\n });\n\n composeForm.addEventListener('submit', function (event) {\n event.preventDefault();\n handleSendRequest();\n });\n\n retryButton.addEventListener('click', function () {\n if (sent) return;\n if (sending) return;\n if (sendBlocked) return;\n var payload = readFormPayload();\n var validationError = validatePayload(payload);\n if (validationError) {\n showError(validationError);\n return;\n }\n sendPayload(payload);\n });\n\n cancelButton.addEventListener('click', function () {\n if (sending) return;\n // Hiding the notice does not resolve whether an earlier send went out.\n var collapseMessage = sendMayHaveGone\n ? 'Collapsed. Check whether it sent if you are not sure.'\n : 'Draft collapsed. Nothing sent.';\n clearError();\n clearSuccess();\n clearUnknown();\n setCollapsed(true, collapseMessage);\n });\n\n reopenButton.addEventListener('click', function () {\n setCollapsed(false);\n if (!sent && sendMayHaveGone) showSendUnknown();\n });\n\n sentCollapseButton.addEventListener('click', function () {\n setCollapsed(true, collapsedMessage.textContent || 'Message sent.');\n });\n\n [toInput, bodyInput].forEach(function (element) {\n element.addEventListener('input', function () {\n if (!sendBlocked) clearError();\n clearSuccess();\n if (!sendMayHaveGone) clearUnknown();\n postResize();\n });\n });\n\n window.addEventListener('message', function (event) {\n // Honest scope (origin-independent): the iframe sandbox origin is 'null',\n // so validating event.origin is unreliable across dev/prod/custom-scheme\n // hosts. Instead reject any *identified* sender that isn't our host frame\n // (window.parent). A real cross-frame postMessage always stamps\n // event.source with the sending window, so a hostile sibling frame is\n // rejected here; a null source only occurs for same-context synthetic\n // dispatch (our own code) and cannot be forged from outside, so we\n // tolerate it. This does not authenticate the host's identity \u2014 the host\n // separately authenticates this iframe by origin+source.\n if (event.source && event.source !== window.parent) {\n return;\n }\n\n var data = event.data;\n if (!data || typeof data !== 'object') {\n return;\n }\n\n if (data.method === 'mcp-app:card-outcome') {\n if (event.source === window.parent) applyCardOutcome(data.params);\n return;\n }\n\n if (data.method === 'ui/notifications/tool-result') {\n // Always parse (so the migration-shim's malformed-envelope warning\n // still fires), but apply only the first successful prefill: a stray\n // or hostile re-post can't silently rewrite a draft the user has\n // already reviewed.\n var draft = getDraftFromToolResult(data.params);\n if (draft && !draftApplied) {\n draftApplied = true;\n applyDraftData(draft);\n }\n return;\n }\n\n if (data.jsonrpc !== '2.0') return;\n var completedSendPayload = null;\n if (pendingSendRequestId && data.id === pendingSendRequestId) {\n completedSendPayload = pendingSendPayload;\n pendingSendPayload = null;\n pendingSendRequestId = null;\n }\n if (!pendingRequestId || data.id !== pendingRequestId) {\n return;\n }\n\n pendingRequestId = null;\n clearSendTimeout();\n clearUnknown();\n setSending(false);\n\n if (data.error) {\n var errorMessage = data.error && typeof data.error.message === 'string'\n ? data.error.message\n : 'Failed to send message.';\n var blockedReason = data.error.data && data.error.data.reason;\n var retryCannotChangeBlock = blockedReason === 'user-disabled' || blockedReason === 'admin-disabled' || blockedReason === 'security-policy';\n var blockedBeforeSend = retryCannotChangeBlock || blockedReason === 'not-dispatched';\n if (retryCannotChangeBlock) {\n sendBlocked = true;\n sendButton.disabled = true;\n }\n if ((!blockedBeforeSend || sendMayHaveGone)) {\n showSendUnknown();\n }\n showError(errorMessage);\n return;\n }\n\n clearError();\n clearSuccess();\n // Preserve what was sent instead of discarding the draft. completedSendPayload\n // is the exact payload we posted. We default to the collapsed summary, but\n // Reopen now restores a read-only view of the sent message.\n var sentPayload = completedSendPayload || readFormPayload();\n var stamp = formatSentTime(Date.now());\n renderSentView(sentPayload, getSendMetaFromResult(data.result), stamp);\n sent = true;\n sendMayHaveGone = false;\n reopenButton.classList.remove('hidden');\n setCollapsed(true, stamp ? 'Message sent \u00B7 ' + stamp : 'Message sent.');\n });\n\n if (typeof ResizeObserver === 'function' && document.body) {\n var observer = new ResizeObserver(postResize);\n observer.observe(document.body);\n }\n\n applyThemeFromHostContext();\n var hostOutcome = (window.__MCP_HOST_CONTEXT__ || {}).cardOutcome;\n if (hostOutcome) applyCardOutcome(hostOutcome);\n else setCollapsed(false);\n postResize();\n postReady();\n\n window.parent.postMessage(\n {\n jsonrpc: '2.0',\n method: 'ui/initialize',\n params: { resourceUri: RESOURCE_URI },\n id: 'compose-email-init'\n },\n '*'\n );\n })();\n </script>\n</body>\n</html>\n";
|
|
2
2
|
//# sourceMappingURL=compose-message-template.d.ts.map
|
|
@@ -423,11 +423,15 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
423
423
|
// already reviewed before clicking Send.
|
|
424
424
|
var draftApplied = false;
|
|
425
425
|
var pendingRequestId = null;
|
|
426
|
-
var
|
|
426
|
+
var pendingSendPayload = null;
|
|
427
|
+
// Retain across the local timeout so even a late reply clears its payload.
|
|
428
|
+
var pendingSendRequestId = null;
|
|
427
429
|
var sending = false;
|
|
430
|
+
var hostSending = false;
|
|
428
431
|
var collapsed = false;
|
|
429
|
-
var
|
|
432
|
+
var sendMayHaveGone = false;
|
|
430
433
|
var sent = false;
|
|
434
|
+
var sendBlocked = false;
|
|
431
435
|
var sendTimeoutId = null;
|
|
432
436
|
// Just beyond the host-side tool-call timeout (60s) so a legitimately slow
|
|
433
437
|
// send resolves to its real result first; this only fires when the reply
|
|
@@ -576,8 +580,11 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
576
580
|
}
|
|
577
581
|
|
|
578
582
|
function setSending(nextSending) {
|
|
583
|
+
// Only a host outcome can release another copy's in-flight send.
|
|
584
|
+
nextSending = nextSending || hostSending;
|
|
579
585
|
sending = nextSending;
|
|
580
|
-
sendButton.disabled = nextSending;
|
|
586
|
+
sendButton.disabled = nextSending || sendBlocked;
|
|
587
|
+
retryButton.disabled = nextSending;
|
|
581
588
|
cancelButton.disabled = nextSending;
|
|
582
589
|
toInput.disabled = nextSending;
|
|
583
590
|
bodyInput.disabled = nextSending;
|
|
@@ -593,6 +600,7 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
593
600
|
}
|
|
594
601
|
|
|
595
602
|
function showError(message) {
|
|
603
|
+
retryButton.classList.toggle('hidden', sendBlocked);
|
|
596
604
|
errorText.textContent = message || 'Failed to send message.';
|
|
597
605
|
errorBox.classList.remove('hidden');
|
|
598
606
|
postResize();
|
|
@@ -617,10 +625,10 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
617
625
|
}
|
|
618
626
|
|
|
619
627
|
function clearUnknown() {
|
|
620
|
-
outcomeUnknown = false;
|
|
621
628
|
unknownTitle.textContent = '';
|
|
622
629
|
unknownText.textContent = '';
|
|
623
630
|
unknownBox.classList.add('hidden');
|
|
631
|
+
errorBox.appendChild(retryButton);
|
|
624
632
|
}
|
|
625
633
|
|
|
626
634
|
// No reply ever came back for the send (e.g. the host's response was lost
|
|
@@ -629,18 +637,19 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
629
637
|
// tell the user to verify before resending (the send is not idempotent, so
|
|
630
638
|
// a blind retry could send a duplicate).
|
|
631
639
|
function showSendUnknown() {
|
|
632
|
-
|
|
640
|
+
sendMayHaveGone = true;
|
|
633
641
|
clearError();
|
|
634
642
|
clearSuccess();
|
|
635
643
|
unknownTitle.textContent = 'Not sure if this sent.';
|
|
636
|
-
unknownText.textContent = "
|
|
644
|
+
unknownText.textContent = "No confirmation came back in time. Check whether it sent before sending again, so it doesn't go out twice.";
|
|
637
645
|
unknownBox.classList.remove('hidden');
|
|
646
|
+
if (!sendBlocked) unknownBox.appendChild(retryButton);
|
|
638
647
|
postResize();
|
|
639
648
|
}
|
|
640
649
|
|
|
641
650
|
function handleSendTimeout() {
|
|
642
651
|
sendTimeoutId = null;
|
|
643
|
-
if (!pendingRequestId) {
|
|
652
|
+
if (!pendingRequestId || sent) {
|
|
644
653
|
return;
|
|
645
654
|
}
|
|
646
655
|
pendingRequestId = null;
|
|
@@ -668,8 +677,9 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
668
677
|
}
|
|
669
678
|
|
|
670
679
|
function sendPayload(payload) {
|
|
671
|
-
|
|
680
|
+
pendingSendPayload = payload;
|
|
672
681
|
pendingRequestId = 'send-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
|
682
|
+
pendingSendRequestId = pendingRequestId;
|
|
673
683
|
setSending(true);
|
|
674
684
|
clearError();
|
|
675
685
|
clearSuccess();
|
|
@@ -683,6 +693,7 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
683
693
|
method: 'tools/call',
|
|
684
694
|
params: {
|
|
685
695
|
name: 'send_chat_message',
|
|
696
|
+
_meta: { 'mcp-app/cardOutcome': 'sent' },
|
|
686
697
|
arguments: payload
|
|
687
698
|
},
|
|
688
699
|
id: pendingRequestId
|
|
@@ -788,9 +799,12 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
788
799
|
}
|
|
789
800
|
}
|
|
790
801
|
|
|
791
|
-
function formatSentTime() {
|
|
802
|
+
function formatSentTime(at) {
|
|
803
|
+
if (typeof at !== 'number' || !Number.isFinite(at)) return '';
|
|
804
|
+
var date = new Date(at);
|
|
805
|
+
if (!Number.isFinite(date.getTime())) return '';
|
|
792
806
|
try {
|
|
793
|
-
return
|
|
807
|
+
return date.toLocaleString(undefined, {
|
|
794
808
|
month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit'
|
|
795
809
|
});
|
|
796
810
|
} catch (_) {
|
|
@@ -798,6 +812,41 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
798
812
|
}
|
|
799
813
|
}
|
|
800
814
|
|
|
815
|
+
function applyCardOutcome(o) {
|
|
816
|
+
if (sent) return;
|
|
817
|
+
if (pendingRequestId && pendingRequestId === pendingSendRequestId &&
|
|
818
|
+
(!o || o.outcome === 'sending' || o.outcome === 'maybe-sent')) return;
|
|
819
|
+
if (hostSending) {
|
|
820
|
+
hostSending = false;
|
|
821
|
+
setSending(false);
|
|
822
|
+
if (sendMayHaveGone) showSendUnknown();
|
|
823
|
+
}
|
|
824
|
+
if (!o || typeof o !== 'object') return;
|
|
825
|
+
if (o.outcome === 'sending') {
|
|
826
|
+
hostSending = true;
|
|
827
|
+
setCollapsed(false);
|
|
828
|
+
setSending(true);
|
|
829
|
+
clearUnknown();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (o.outcome === 'maybe-sent') {
|
|
833
|
+
showSendUnknown();
|
|
834
|
+
setCollapsed(false);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (o.outcome !== 'sent' && o.outcome !== 'submitted') return;
|
|
838
|
+
sent = true;
|
|
839
|
+
sendMayHaveGone = false;
|
|
840
|
+
clearSendTimeout();
|
|
841
|
+
clearError();
|
|
842
|
+
clearSuccess();
|
|
843
|
+
clearUnknown();
|
|
844
|
+
var stamp = formatSentTime(o.at);
|
|
845
|
+
setCollapsed(true, stamp ? 'Message sent · ' + stamp : 'Message sent.');
|
|
846
|
+
if (pendingSendPayload) renderSentView(pendingSendPayload, null, stamp);
|
|
847
|
+
reopenButton.classList.toggle('hidden', !pendingSendPayload);
|
|
848
|
+
}
|
|
849
|
+
|
|
801
850
|
function renderSentView(payload, meta, stamp) {
|
|
802
851
|
payload = payload && typeof payload === 'object' ? payload : {};
|
|
803
852
|
sentTimestamp.textContent = stamp ? 'Sent ' + stamp : 'Sent';
|
|
@@ -806,8 +855,9 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
806
855
|
}
|
|
807
856
|
|
|
808
857
|
function handleSendRequest() {
|
|
858
|
+
if (sent) return;
|
|
809
859
|
if (sending) return;
|
|
810
|
-
|
|
860
|
+
if (sendBlocked) return;
|
|
811
861
|
var payload = readFormPayload();
|
|
812
862
|
var validationError = validatePayload(payload);
|
|
813
863
|
if (validationError) {
|
|
@@ -819,7 +869,7 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
819
869
|
}
|
|
820
870
|
|
|
821
871
|
// The Send button is type="button" with an explicit click handler rather
|
|
822
|
-
// than a form submit button. The
|
|
872
|
+
// than a form submit button. The host renders this iframe with a
|
|
823
873
|
// sandbox of "allow-scripts" only — no "allow-forms" — so native form
|
|
824
874
|
// submission is blocked by the browser BEFORE the submit event fires
|
|
825
875
|
// ("Blocked form submission ... the form's frame is sandboxed and the
|
|
@@ -840,7 +890,9 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
840
890
|
});
|
|
841
891
|
|
|
842
892
|
retryButton.addEventListener('click', function () {
|
|
893
|
+
if (sent) return;
|
|
843
894
|
if (sending) return;
|
|
895
|
+
if (sendBlocked) return;
|
|
844
896
|
var payload = readFormPayload();
|
|
845
897
|
var validationError = validatePayload(payload);
|
|
846
898
|
if (validationError) {
|
|
@@ -852,9 +904,8 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
852
904
|
|
|
853
905
|
cancelButton.addEventListener('click', function () {
|
|
854
906
|
if (sending) return;
|
|
855
|
-
//
|
|
856
|
-
|
|
857
|
-
var collapseMessage = outcomeUnknown
|
|
907
|
+
// Hiding the notice does not resolve whether an earlier send went out.
|
|
908
|
+
var collapseMessage = sendMayHaveGone
|
|
858
909
|
? 'Collapsed. Check whether it sent if you are not sure.'
|
|
859
910
|
: 'Draft collapsed. Nothing sent.';
|
|
860
911
|
clearError();
|
|
@@ -865,6 +916,7 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
865
916
|
|
|
866
917
|
reopenButton.addEventListener('click', function () {
|
|
867
918
|
setCollapsed(false);
|
|
919
|
+
if (!sent && sendMayHaveGone) showSendUnknown();
|
|
868
920
|
});
|
|
869
921
|
|
|
870
922
|
sentCollapseButton.addEventListener('click', function () {
|
|
@@ -873,9 +925,9 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
873
925
|
|
|
874
926
|
[toInput, bodyInput].forEach(function (element) {
|
|
875
927
|
element.addEventListener('input', function () {
|
|
876
|
-
clearError();
|
|
928
|
+
if (!sendBlocked) clearError();
|
|
877
929
|
clearSuccess();
|
|
878
|
-
clearUnknown();
|
|
930
|
+
if (!sendMayHaveGone) clearUnknown();
|
|
879
931
|
postResize();
|
|
880
932
|
});
|
|
881
933
|
});
|
|
@@ -899,6 +951,11 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
899
951
|
return;
|
|
900
952
|
}
|
|
901
953
|
|
|
954
|
+
if (data.method === 'mcp-app:card-outcome') {
|
|
955
|
+
if (event.source === window.parent) applyCardOutcome(data.params);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
902
959
|
if (data.method === 'ui/notifications/tool-result') {
|
|
903
960
|
// Always parse (so the migration-shim's malformed-envelope warning
|
|
904
961
|
// still fires), but apply only the first successful prefill: a stray
|
|
@@ -912,7 +969,14 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
912
969
|
return;
|
|
913
970
|
}
|
|
914
971
|
|
|
915
|
-
if (data.jsonrpc !== '2.0'
|
|
972
|
+
if (data.jsonrpc !== '2.0') return;
|
|
973
|
+
var completedSendPayload = null;
|
|
974
|
+
if (pendingSendRequestId && data.id === pendingSendRequestId) {
|
|
975
|
+
completedSendPayload = pendingSendPayload;
|
|
976
|
+
pendingSendPayload = null;
|
|
977
|
+
pendingSendRequestId = null;
|
|
978
|
+
}
|
|
979
|
+
if (!pendingRequestId || data.id !== pendingRequestId) {
|
|
916
980
|
return;
|
|
917
981
|
}
|
|
918
982
|
|
|
@@ -925,19 +989,31 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
925
989
|
var errorMessage = data.error && typeof data.error.message === 'string'
|
|
926
990
|
? data.error.message
|
|
927
991
|
: 'Failed to send message.';
|
|
992
|
+
var blockedReason = data.error.data && data.error.data.reason;
|
|
993
|
+
var retryCannotChangeBlock = blockedReason === 'user-disabled' || blockedReason === 'admin-disabled' || blockedReason === 'security-policy';
|
|
994
|
+
var blockedBeforeSend = retryCannotChangeBlock || blockedReason === 'not-dispatched';
|
|
995
|
+
if (retryCannotChangeBlock) {
|
|
996
|
+
sendBlocked = true;
|
|
997
|
+
sendButton.disabled = true;
|
|
998
|
+
}
|
|
999
|
+
if ((!blockedBeforeSend || sendMayHaveGone)) {
|
|
1000
|
+
showSendUnknown();
|
|
1001
|
+
}
|
|
928
1002
|
showError(errorMessage);
|
|
929
1003
|
return;
|
|
930
1004
|
}
|
|
931
1005
|
|
|
932
1006
|
clearError();
|
|
933
1007
|
clearSuccess();
|
|
934
|
-
// Preserve what was sent instead of discarding the draft.
|
|
1008
|
+
// Preserve what was sent instead of discarding the draft. completedSendPayload
|
|
935
1009
|
// is the exact payload we posted. We default to the collapsed summary, but
|
|
936
1010
|
// Reopen now restores a read-only view of the sent message.
|
|
937
|
-
var sentPayload =
|
|
938
|
-
var stamp = formatSentTime();
|
|
1011
|
+
var sentPayload = completedSendPayload || readFormPayload();
|
|
1012
|
+
var stamp = formatSentTime(Date.now());
|
|
939
1013
|
renderSentView(sentPayload, getSendMetaFromResult(data.result), stamp);
|
|
940
1014
|
sent = true;
|
|
1015
|
+
sendMayHaveGone = false;
|
|
1016
|
+
reopenButton.classList.remove('hidden');
|
|
941
1017
|
setCollapsed(true, stamp ? 'Message sent · ' + stamp : 'Message sent.');
|
|
942
1018
|
});
|
|
943
1019
|
|
|
@@ -947,7 +1023,9 @@ export const COMPOSE_MESSAGE_HTML = `
|
|
|
947
1023
|
}
|
|
948
1024
|
|
|
949
1025
|
applyThemeFromHostContext();
|
|
950
|
-
|
|
1026
|
+
var hostOutcome = (window.__MCP_HOST_CONTEXT__ || {}).cardOutcome;
|
|
1027
|
+
if (hostOutcome) applyCardOutcome(hostOutcome);
|
|
1028
|
+
else setCollapsed(false);
|
|
951
1029
|
postResize();
|
|
952
1030
|
postReady();
|
|
953
1031
|
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
*/
|
|
21
21
|
/**
|
|
22
22
|
* Wrap a single untrusted string in an `<untrusted-content source="…">`
|
|
23
|
-
* envelope, escaping any embedded close-tag variant so the envelope
|
|
24
|
-
* broken out of.
|
|
23
|
+
* envelope, escaping any embedded open/close-tag variant so the envelope can
|
|
24
|
+
* neither be broken out of nor have a spoofed nested envelope planted inside.
|
|
25
25
|
*
|
|
26
26
|
* `undefined` and `null` are passed through untouched so callers can apply the wrapper
|
|
27
27
|
* uniformly to optional fields without branching.
|
|
28
28
|
*
|
|
29
29
|
* Idempotent: when `text` is already a properly-shaped envelope for the SAME
|
|
30
30
|
* `source` (starts with the matching OPEN tag, ends with CLOSE, and contains no
|
|
31
|
-
* internal close-tag variants), the original string is returned unchanged so
|
|
31
|
+
* internal open/close-tag variants), the original string is returned unchanged so
|
|
32
32
|
* `wrapUntrusted(wrapUntrusted(s, src), src) === wrapUntrusted(s, src)`.
|
|
33
33
|
*/
|
|
34
34
|
export declare function wrapUntrusted(text: string | null | undefined, source: string): string | undefined;
|
|
@@ -18,34 +18,44 @@
|
|
|
18
18
|
* `untrusted-content` in any connector that talks to an external system; this
|
|
19
19
|
* file (and the call sites that import from it) is what satisfies that gate.
|
|
20
20
|
*/
|
|
21
|
-
const UNTRUSTED_CLOSE_TAG_VARIANT = /<\/untrusted-content
|
|
21
|
+
const UNTRUSTED_CLOSE_TAG_VARIANT = /<\/untrusted-content(?:\s[^>]*)?>/gi;
|
|
22
22
|
const ESCAPED_UNTRUSTED_CLOSE_TAG = '<\\/untrusted-content>';
|
|
23
|
+
const UNTRUSTED_OPEN_TAG_VARIANT = /<untrusted-content(?:\s[^>]*)?>/gi;
|
|
24
|
+
const ESCAPED_UNTRUSTED_OPEN_TAG = '<\\untrusted-content>';
|
|
23
25
|
const UNTRUSTED_ENVELOPE = /^<untrusted-content source="[^"]*">([\s\S]*)<\/untrusted-content>$/;
|
|
24
26
|
function escapeAttr(s) {
|
|
25
27
|
return s.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
|
-
* Rewrite every `</untrusted-content>`
|
|
29
|
-
* whitespace before `>`) inside `s`
|
|
30
|
-
*
|
|
30
|
+
* Rewrite every `</untrusted-content>` and `<untrusted-content …>` variant
|
|
31
|
+
* (case-insensitive, optional whitespace / attributes before `>`) inside `s`
|
|
32
|
+
* to a benign textual form, so an attacker who controls the wrapped content
|
|
33
|
+
* can neither terminate the envelope early nor spoof a nested open tag that a
|
|
34
|
+
* downstream parser could re-read as a fresh envelope. An LLM parsing the
|
|
35
|
+
* markup plausibly accepts attribute-bearing close forms such as
|
|
36
|
+
* `</untrusted-content foo>` — the matcher must cover them too.
|
|
31
37
|
*/
|
|
32
|
-
function
|
|
33
|
-
return s
|
|
38
|
+
function escapeTagSentinels(s) {
|
|
39
|
+
return s
|
|
40
|
+
.replace(UNTRUSTED_OPEN_TAG_VARIANT, ESCAPED_UNTRUSTED_OPEN_TAG)
|
|
41
|
+
.replace(UNTRUSTED_CLOSE_TAG_VARIANT, ESCAPED_UNTRUSTED_CLOSE_TAG);
|
|
34
42
|
}
|
|
35
|
-
function
|
|
36
|
-
return s
|
|
43
|
+
function unescapeTagSentinels(s) {
|
|
44
|
+
return s
|
|
45
|
+
.replaceAll(ESCAPED_UNTRUSTED_OPEN_TAG, '<untrusted-content>')
|
|
46
|
+
.replaceAll(ESCAPED_UNTRUSTED_CLOSE_TAG, '</untrusted-content>');
|
|
37
47
|
}
|
|
38
48
|
/**
|
|
39
49
|
* Wrap a single untrusted string in an `<untrusted-content source="…">`
|
|
40
|
-
* envelope, escaping any embedded close-tag variant so the envelope
|
|
41
|
-
* broken out of.
|
|
50
|
+
* envelope, escaping any embedded open/close-tag variant so the envelope can
|
|
51
|
+
* neither be broken out of nor have a spoofed nested envelope planted inside.
|
|
42
52
|
*
|
|
43
53
|
* `undefined` and `null` are passed through untouched so callers can apply the wrapper
|
|
44
54
|
* uniformly to optional fields without branching.
|
|
45
55
|
*
|
|
46
56
|
* Idempotent: when `text` is already a properly-shaped envelope for the SAME
|
|
47
57
|
* `source` (starts with the matching OPEN tag, ends with CLOSE, and contains no
|
|
48
|
-
* internal close-tag variants), the original string is returned unchanged so
|
|
58
|
+
* internal open/close-tag variants), the original string is returned unchanged so
|
|
49
59
|
* `wrapUntrusted(wrapUntrusted(s, src), src) === wrapUntrusted(s, src)`.
|
|
50
60
|
*/
|
|
51
61
|
export function wrapUntrusted(text, source) {
|
|
@@ -55,13 +65,14 @@ export function wrapUntrusted(text, source) {
|
|
|
55
65
|
const close = '</untrusted-content>';
|
|
56
66
|
if (text.startsWith(open) && text.endsWith(close) && text.length >= open.length + close.length) {
|
|
57
67
|
const inner = text.slice(open.length, text.length - close.length);
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
const hasVariant = UNTRUSTED_CLOSE_TAG_VARIANT.test(inner) || UNTRUSTED_OPEN_TAG_VARIANT.test(inner);
|
|
69
|
+
UNTRUSTED_CLOSE_TAG_VARIANT.lastIndex = 0; // reset stateful /g regexes
|
|
70
|
+
UNTRUSTED_OPEN_TAG_VARIANT.lastIndex = 0;
|
|
71
|
+
if (!hasVariant) {
|
|
60
72
|
return text;
|
|
61
73
|
}
|
|
62
|
-
UNTRUSTED_CLOSE_TAG_VARIANT.lastIndex = 0;
|
|
63
74
|
}
|
|
64
|
-
return `${open}${
|
|
75
|
+
return `${open}${escapeTagSentinels(text)}${close}`;
|
|
65
76
|
}
|
|
66
77
|
/**
|
|
67
78
|
* Strip one `<untrusted-content>` envelope from `text` if present, returning raw
|
|
@@ -73,7 +84,7 @@ export function unwrapUntrusted(text) {
|
|
|
73
84
|
const match = UNTRUSTED_ENVELOPE.exec(text);
|
|
74
85
|
if (!match)
|
|
75
86
|
return text;
|
|
76
|
-
return
|
|
87
|
+
return unescapeTagSentinels(match[1]);
|
|
77
88
|
}
|
|
78
89
|
/**
|
|
79
90
|
* Recursively wrap every string key and value reachable inside `value` (strings,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindstone/mcp-server-microsoft-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"mcpName": "io.github.mindstone/mcp-server-microsoft-teams",
|
|
5
5
|
"description": "Microsoft 365 Teams via Graph: list chats, read/send messages, list teams/channels, presence.",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.mindstone/mcp-server-microsoft-teams",
|
|
4
4
|
"title": "Microsoft 365 Teams",
|
|
5
5
|
"description": "Microsoft 365 Teams via Graph: list chats, read/send messages, list teams/channels, presence.",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.2",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/mindstone/mcp-servers",
|
|
9
9
|
"source": "github",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "@mindstone/mcp-server-microsoft-teams",
|
|
17
|
-
"version": "0.3.
|
|
17
|
+
"version": "0.3.2",
|
|
18
18
|
"runtimeHint": "npx",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|