slackapi / slackapi/slack-skills-plugin

slack_send_message_draft: [label](url) silently drops the URL; inline formatting dropped; bare URLs emit broken mrkdwn

Open
#121 1 comment 0 reactions 1 assignee View on GitHub

@hello-ashleyintech is already working on this.

Since Aug 17, 2026.

bug
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-official v1.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 inner closes 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_draft strips newlines (same tool, same draft-vs-send asymmetry)
  • #33 — slack_send_message refused 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.