Skip to content

Latest commit

 

History

History
120 lines (103 loc) · 5.79 KB

File metadata and controls

120 lines (103 loc) · 5.79 KB

mcp-slack-block-kit — Claude Code project notes

Single-binary MCP server + CLI that converts AI-generated markdown into Slack Block Kit JSON. Go 1.25+. Zero external runtime dependencies.

Long-form architecture and design rationale: @docs/CLAUDE-architecture.md

Build / test / lint commands

Action Command
Build local binary make build (output: bin/mcp-slack-block-kit)
Run all tests make test
Race tests + coverage make test-race
HTML coverage report make cover
Lint (golangci-lint v2) make lint
Format gofumpt -w .
Vet make vet
Vulnerability scan make vuln
Fuzz the splitter (30s) make fuzz
GoReleaser snapshot make snapshot
Install lefthook hooks make setup (one-time after clone)

CI mirrors all of the above. Coverage gate: ≥80% overall, enforced in .github/workflows/ci.yml. Per-package targets are documented in CONTRIBUTING.md.

Code style

  • Format: gofumpt (stricter superset of gofmt). Tabs for indentation.
  • Lint: golangci-lint v2 with the config in .golangci.yml. v2 bundles staticcheck, errcheck, govet, gosec, gocritic. Don't add a standalone gosec hook — it duplicates.
  • Comments: comment the why, not the what. No autogenerated docstrings. Don't restate function signatures.
  • Tests: table-driven ([]struct{name, in, want}), one t.Run per row. Use cmp.Diff for nested-struct asserts; require/assert are not in this repo's dep tree.
  • Errors: wrap with fmt.Errorf("...: %w", err). Sentinel errors via errors.Is/As. At MCP boundaries, return IsError: true on CallToolResult for tool-level failures (per spec); reserve JSON-RPC transport errors for protocol-level problems.
  • Logging: log/slog JSON handler to stderr only. The MCP stdio transport reserves stdout for protocol bytes — anything else corrupts the channel.

Security-critical rules

  • Mention sanitization is mandatory. Every text run emitted into a Slack text field must HTML-entity-escape & < > unless Options.AllowBroadcasts == true. Without this, AI-generated content containing literal <!channel> / <!here> / <@U…> would broadcast or ping the workspace. Path-scoped rule: see .claude/rules/security.md. Options.PreserveMentionTokens is the narrower escape hatch (typed Slack tokens pass through; catastrophic broadcasts still escape) — the safer alternative to AllowBroadcasts when the markdown comes from a trusted Slack tool result.
  • Bound input. Options.MaxInputBytes (default 256 KiB) keeps goldmark from allocating gigabytes on hostile input. Don't bypass.
  • HTTP/SSE transports default to localhost. When wiring a new HTTP feature, keep the SDK's DNS-rebinding protection enabled; don't set WriteTimeout on the wrapping http.Server (kills SSE GETs); use crypto/subtle.ConstantTimeCompare for any token comparison.
  • No secrets in this repo. The server holds none. Don't log os.Environ(), request bodies at INFO, or anything that might contain user PII. The bearer-token middleware logs request lines but never the token value.

Repository layout

cmd/mcp-slack-block-kit/   cobra entry point (server default + convert subcommand)
internal/converter/       goldmark renderer + emoji/mentions/markdown_block
internal/reverse/         Block Kit → markdown (inverse of converter; lossy)
internal/validator/       slack constraint suite + structured Violations
internal/splitter/        SplitText + ChunkBlocks (50-block + table-isolation)
internal/preview/         Block Kit Builder URL encoder
internal/server/          MCP wiring (6 tools + cheatsheet resource + prompt)
block_kit/                 Public Go library re-exports (semver-stable surface)
docs/                     Public docs (CLAUDE-architecture.md, etc.)
docs/internal/            Gitignored design notes (research.md lives here)

Workflow conventions

  • Branches: feat/<slug>, fix/<slug>, chore/<slug>, docs/<slug>.
  • Commits: Conventional Commits 1.0 (enforced by the lefthook commit-msg hook). Examples: feat(converter): add task-list checkbox, fix(server): handle nil mention map.
  • PRs: every change has at least one test. The lefthook pre-push hook runs the race suite + govulncheck before the push lands.
  • Releases: tag-driven via GoReleaser. See .claude/skills/release/ (invoke with /release) for the bump-tag-push flow.

Common gotchas

  • slack-go/slack v0.23.0's RichTextPreformatted.Language field is emitted into JSON, but Slack itself does not syntax-highlight. We preserve the tag for tooling and explicitly document that Slack ignores it.
  • The MCP SDK's jsonschema-go infers json.RawMessage as integer-array (because it's []byte). Use any for fields that carry block payloads; see internal/server/convert_tool.go for the pattern.
  • The auto-mode picker chooses the new markdown block (Feb 2025) for short LLM outputs. It will skip that path if it sees *ast.Image or a table over Slack's row/col limits — verify by reading internal/converter/markdown_block.go::shouldUseMarkdownBlock.
  • Do not pair parallel: true with multiple stage_fixed: true commands in lefthook.yml — they race on the git index. Our config groups them in piped: true order.

Don't

  • Don't add a co-author trailer for Claude in commits (your global ~/.claude/CLAUDE.md enforces this).
  • Don't fork or vendor competitor libraries (navidemad/md2slack, takara2314/slack-go-util). Read them as references; reimplement here. See feedback memory for the why.
  • Don't introduce new dependencies without checking govulncheck and considering supply-chain weight. The current dep tree is intentionally minimal.