feat(mcp): render markdown links in helperText; update Slack catalog pin (#1012)

* feat(mcp): render markdown links in helperText; bump extensions to slack field-order PR commit

- Add renderHelperText() to install-server-modal.tsx that converts
  [text](url) patterns into <a> elements with target=_blank, so the
  Slack workspace-ID helper text (and any future catalog entries) can
  embed clickable docs links inline.
- Bump @openhands/extensions to commit 2d43e9c (branch
  slack-catalog-field-order-and-helper-links, PR #285) which:
    • moves SLACK_TEAM_ID before SLACK_BOT_TOKEN in the install modal
    • replaces the plain SLACK_TEAM_ID helper text with linked copy:
      'First visit [here](...#find-your-url) to get your Slack URL
       and then visit [here](...#find-your-workspace-or-org-id) to
       get your workspace ID.'
- Removes stale integrity hash from package-lock.json for the
  @openhands/extensions entry; npm install will recompute it.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to d186872 (SLACK_BOT_TOKEN helperText)

Add inline linked helperText for SLACK_BOT_TOKEN in slack.json (PR #285,
commit d186872): 'You'll need to create or update a Slack App as shown
[here](https://github.com/zencoderai/slack-mcp-server#slack-bot-setup).'
Drops the now-redundant helperLink field.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to b45d3a1 (SLACK_TEAM_ID helperText rewrite)

Update SLACK_TEAM_ID helperText to named links:
'First get your [Slack URL](...). Then use that to get your [Workspace ID](...).'

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to 84a0a6e (SLACK_BOT_TOKEN named link)

Update SLACK_BOT_TOKEN helperText to:
"You'll need to create or update a [Slack App](...#slack-bot-setup)."

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to e07f427 (SLACK_BOT_TOKEN helperText)

Update SLACK_BOT_TOKEN helperText to:
"You'll need to create or update a [Slack App](...) to get a Bot token"

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to 5efd1b8

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to 952c759

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to f30dbfb

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to 02715f4

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: bump @openhands/extensions to cb092c8

Sync to latest commit on slack-catalog-field-order-and-helper-links (PR #285).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(mcp): validate URL scheme in renderHelperText; use matchAll

- Guard href against javascript:/data: XSS via /^https?:\/\//i test
- Replace exec-in-while with matchAll to drop the eslint-disable comment

Addresses review bot feedback on PR #1012.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(mcp): use double quotes for fallback href to satisfy Prettier

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: update @openhands/extensions to latest main (62594156)

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
chuckbutkus
2026-06-02 22:52:22 +00:00
committed by GitHub
co-authored by openhands
parent 667c5162fa
commit c0c413f406
3 changed files with 43 additions and 6 deletions
+3 -3
View File
@@ -12,7 +12,7 @@
"@heroui/react": "2.8.10",
"@microlink/react-json-view": "1.31.20",
"@monaco-editor/react": "4.7.0",
"@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#e14f740c59b4bfd7369d4bb6aea5eeb33dd05909",
"@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#62594156a187722344736bc2ff5edfb12c0cc75c",
"@openhands/typescript-client": "1.24.3",
"@react-router/node": "7.14.2",
"@react-router/serve": "7.14.2",
@@ -3441,8 +3441,8 @@
},
"node_modules/@openhands/extensions": {
"version": "0.0.0",
"resolved": "git+https://github.com/OpenHands/extensions.git#e14f740c59b4bfd7369d4bb6aea5eeb33dd05909",
"integrity": "sha512-PC0pmJD1AiNP9aDfdDfDPP1iF40m/QN3vyv/xPN9sigLBtCmNBugSIySWEGVIGydARYkVi+NHL2KorPtlAOFAg==",
"resolved": "git+https://github.com/OpenHands/extensions.git#62594156a187722344736bc2ff5edfb12c0cc75c",
"integrity": "sha512-K5XnP/YX8vVV/3VWWbT6QA+fVzlil63EL5KRO77HU+FrKFCMTJTOCJH7pxwCoAJGa0Wu0hV+udpcRYI+une35A==",
"license": "MIT",
"engines": {
"node": ">=18.20.0"
+1 -1
View File
@@ -23,7 +23,7 @@
"@heroui/react": "2.8.10",
"@microlink/react-json-view": "1.31.20",
"@monaco-editor/react": "4.7.0",
"@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#e14f740c59b4bfd7369d4bb6aea5eeb33dd05909",
"@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#62594156a187722344736bc2ff5edfb12c0cc75c",
"@openhands/typescript-client": "1.24.3",
"@react-router/node": "7.14.2",
"@react-router/serve": "7.14.2",
@@ -23,6 +23,39 @@ import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"
import { useSaveFieldsAsSecrets } from "#/hooks/mutation/use-save-fields-as-secrets";
import { modalTitleLgClassName } from "#/utils/modal-classes";
/**
* Renders a helperText string as React nodes, converting any `[text](url)`
* markdown links into real `<a>` elements. Plain text segments are left as-is.
* Only `http:` and `https:` URLs are rendered as links; anything else falls
* back to `#` to guard against `javascript:` / `data:` XSS vectors.
*/
function renderHelperText(text: string): React.ReactNode {
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
for (const match of text.matchAll(linkPattern)) {
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
parts.push(
<a
key={match.index}
href={/^https?:\/\//i.test(match[2]) ? match[2] : "#"}
target="_blank"
rel="noreferrer"
className="underline hover:text-white transition-colors"
>
{match[1]}
</a>,
);
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}
return parts;
}
interface InstallServerModalProps {
entry: MarketplaceEntry;
onClose: () => void;
@@ -321,7 +354,9 @@ export function InstallServerModal({
className="w-full"
/>
{field.helperText && (
<p className="text-xs text-tertiary-alt">{field.helperText}</p>
<p className="text-xs text-tertiary-alt">
{renderHelperText(field.helperText)}
</p>
)}
{state.errors[field.key] && (
<p className="text-xs text-red-500">{state.errors[field.key]}</p>
@@ -352,7 +387,9 @@ export function InstallServerModal({
className="w-full"
/>
{field.helperText && (
<p className="text-xs text-tertiary-alt">{field.helperText}</p>
<p className="text-xs text-tertiary-alt">
{renderHelperText(field.helperText)}
</p>
)}
{state.errors[field.key] && (
<p className="text-xs text-red-500">{state.errors[field.key]}</p>