slackapi / slackapi/slack-skills-plugin
slack_send_message_draft: [label](url) silently drops the URL; inline formatting dropped; bare URLs emit broken mrkdwn
@hello-ashleyintech is already working on this.
Since Aug 17, 2026.
- Dominant language
- Python
- Stars
- 132
- Forks
- 34
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 16
Description
Summary
The markdown conversion in the slack_send_message_draft path is broken in four distinct ways. The most serious is silent data loss: [label](url) keeps the label and discards the URL, so the draft looks intentional while the destination is simply gone.
slack_send_message handles the same content correctly, so this is specific to the draft path — the same conclusion reached in #18 for newlines.
This matters disproportionately because drafts are the only available path on Slack Connect (externally shared) channels, where direct sends are refused (#33). On those channels there is no working alternative.
Environment
- MCP server:
https://mcp.slack.com/mcp - Plugin:
slack@claude-plugins-officialv1.2.0 - Client: Claude Code
- Date observed: 2026-08-17
Results
All link and URL cases were confirmed by sending the draft and reading the message back with conversations.history, which is unambiguous — a real link returns as <url|label>. Inline styling was confirmed visually in the composer; note that a plaintext copy out of Slack cannot distinguish "rendered" from "stripped", since both produce bare text.
| Input | Expected | Observed |
|---|---|---|
**bold** |
bold | plain text, markers consumed |
*bold* (Slack mrkdwn) |
bold | plain text, markers consumed |
_italic_ |
italic | plain text, markers consumed |
`code` |
monospace | plain text, markers consumed |
~~strike~~ |
struck | plain text, markers consumed |
> quote at line start |
quote bar | plain text |
## header |
header | plain text |
```bash fenced block |
monospace box | no box; bash leaks as a literal body line |
[label](url) |
link | label only — URL silently dropped, confirmed in the sent message |
| bare URL ending a line, further lines after | link | broken <…> mrkdwn, corrupts the next line |
| bare URL with same-line trailing text | link | works |
| bare URL ending the message | link | works |
- bullet at line start |
bullet | works (•) |
<@USERID> |
mention chip | works |
<@USERID> inside a fence |
mention or literal | invalid_blocks, no draft created |
Bug 1 — [label](url) silently drops the URL
Input:
[PR 12887](https://github.com/ethereum-optimism/k8s/pull/12887)
Observed: the draft shows PR 12887 as ordinary unstyled text. The URL is gone — not mangled, not visible, just absent. Sending the draft and reading it back confirms the loss is real rather than a composer-display artifact: the message text contains the bare string labelled link, with no <url|label> anywhere.
Expected: a link labelled PR 12887 pointing at the URL.
This is the worst of the four because it fails invisibly. The author sees a sensible-looking message and has no signal that the link died. Every other bug here is at least visible.
Bug 2 — bare URL ending a line emits broken mrkdwn
Input:
6 bare url: https://github.com/ethereum-optimism/k8s/pull/12887
7 bullet list:
Observed:
6 bare url: <https://github.com/ethereum-optimism/k8s/pull/12887
7> bullet list:
The URL is wrapped in mrkdwn angle brackets, but the closing > is placed after the newline and the following line's first character. Because <…> spanning a line break is invalid mrkdwn it never becomes a link, the brackets leak as literal characters, and the following line is corrupted.
The trigger is specifically a bare URL terminating a line when a further line follows. Two neighbouring cases render correctly, which localises it to the newline boundary rather than to trailing content generally:
- a URL with trailing text on the same line — returns a correctly delimited
<url>with the following text intact, even when more lines follow - a URL ending the whole message
Bug 3 — inline styling silently dropped, in both dialects
**bold**, _italic_, `code`, ~~strike~~, > blockquote and ## header all lose their markers without any styling being applied. The markers are clearly consumed (they do not appear literally), so a conversion runs — it just produces unstyled output.
This is not a standard-markdown-versus-mrkdwn mismatch: Slack's own *bold* (single asterisk) is eaten identically. Sent and read back, *single-asterisk bold* and **double-asterisk bold** arrives as single-asterisk bold and double-asterisk bold. There is no emphasis syntax that survives this path.
The slack_send_message_draft tool description explicitly advertises this support:
The draft message content using standard markdown. Supports bold, italic,
code,strikethrough, >blockquotes, lists, links, and code blocks.
Bug 4 — <@USERID> inside a code fence returns invalid_blocks
Placing a user mention inside a fenced block fails the call outright:
```
<@U04J4QNR84S>
```
Observed: execution_failed: invalid_blocks, no draft created.
Expected: either the literal text preserved (consistent with all other fenced content, see below) or the mention resolved. A hard failure with an opaque error is the one outcome that gives the caller nothing to act on. Removing the mention from the fence makes the identical call succeed.
Workaround
Wrapping the message body in a ``` fence makes the converter skip it entirely. Content arrives byte-for-byte, and the fence markers themselves are consumed, so the draft is sendable as-is with no cleanup.
Crucially, this is not merely a way to preserve exact characters — the delivered message is fully formatted, provided you write Slack's native mrkdwn inside the fence rather than standard markdown. Verified end-to-end by sending each case and reading it back with conversations.history:
| Inside the fence | Sent result |
|---|---|
*bold* (single asterisk) |
bold |
_italic_ |
italic |
~strike~ (single tilde) |
struck |
`code` |
monospace |
>quoted at the start of a line |
blockquote |
[label](url) |
real labelled link — comes back as <url|label> |
| bare URL | auto-linked — comes back as <url> |
- item |
bullet list |
**bold**, ~~strike~~ (standard markdown) |
literal characters, delimiters visible |
So the fence sidesteps Bugs 1–3 completely and still yields a properly formatted message. The only adjustment is halving the emphasis delimiters, since **bold** is standard markdown and *bold* is what Slack renders.
Limits:
- Mentions must stay outside the fence (Bug 4). Placed before and after one, they resolve correctly.
>quotes and-bullets must begin their line.- The fence must be exactly three backticks. A four-backtick fence is not recognised: the content is converted rather than skipped (emphasis eaten, quotes flattened, an inner
block flattened) *and* the delimiters leak into the sent message as literal `~~~`. Consequently a fenced code block cannot be nested inside the body, since an innercloses the outer fence — a real limitation for sharing commands or config snippets, which is a common reason to want a draft in the first place.
Analysis
Bugs 1–3 look like one defect: a markdown → mrkdwn conversion that recognises constructs well enough to consume their delimiters but does not emit the corresponding Slack formatting, and mishandles URL boundaries. Bug 4 appears to be fence handling in the same converter.
As noted in #18, the fix would need to land on the hosted server at mcp.slack.com rather than in this repository.
One difference from #18 worth recording: newlines and paragraph breaks were preserved correctly in every draft tested here, including multi-paragraph messages. Either that has been fixed since 2026-03, or it is content-dependent.
A methodological caveat for anyone reproducing this, learned the hard way: conversations.history returns a text field that is a lossy fallback for messages composed in the WYSIWYG composer, which are stored as structured blocks. It is authoritative for mrkdwn-encoded constructs such as <url|label> links, but not for block-level layout — a blank line separating two blocks can be absent from text while being present in the message. Verify layout claims against the rendered message, and reserve the API read-back for link and entity encoding.
Related
- #18 —
slack_send_message_draftstrips newlines (same tool, same draft-vs-send asymmetry) - #33 —
slack_send_messagerefused on Slack Connect channels, which is what forces drafts in the first place - #92 — draft creation returns success when a draft already exists
🤖 Co-created with Claude Opus 5
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.