Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/restructure-plugin-into-subdirectory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"slack": patch
---

Restructure the repository so the plugin lives under `plugins/slack/` and add a top-level `.claude-plugin/marketplace.json`, enabling a Slack-hosted marketplace and future multi-plugin layout. No change to the plugin's skills, commands, or MCP configuration.
18 changes: 18 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "slack-skills",
"owner": {
"name": "Slack"
},
"metadata": {
"description": "Official Slack MCP Server and developer skills for AI coding agents"
},
"plugins": [
{
"name": "slack",
"description": "Slack integration for searching messages, sending communications, managing canvases, and more",
"source": "./plugins/slack",
"category": "development",
"tags": ["slack", "mcp", "bolt"]
}
]
}
19 changes: 11 additions & 8 deletions .github/maintainers_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,31 +87,34 @@ make typecheck # Mypy static type checks
```

Markdown linting is powered by [rumdl](https://github.com/rvben/rumdl), a
markdownlint-compatible Rust linter. It validates `skills/`, `commands/`,
`README.md`, and `AGENTS.md`. Rules and disabled checks are configured under
`[tool.rumdl]` in `pyproject.toml` — tune that section when a new skill trips a
rule that isn't worth enforcing.
markdownlint-compatible Rust linter. It validates `plugins/slack/skills/`,
`plugins/slack/commands/`, `README.md`, and `AGENTS.md`. Rules and disabled
checks are configured under `[tool.rumdl]` in `pyproject.toml` — tune that
section when a new skill trips a rule that isn't worth enforcing.

### Testing in Claude Code

Load your local changes into Claude Code for a single session with the
`--plugin-dir` flag:

```sh
claude --plugin-dir ./
claude --plugin-dir ./plugins/slack
```

This loads the `slack` plugin from your checkout — its skills and commands, and
the HTTP MCP server from `.mcp.json`. If you already have the published
the HTTP MCP server from `plugins/slack/.mcp.json`. If you already have the published
`slack` plugin installed, the local copy takes precedence **for that session
only**: nothing is written to your settings, and the installed version is
untouched when you exit. After editing a skill or command, run `/reload-plugins`
inside the session to pick up the change without restarting.

Check the plugin's structure without launching a session:
Check the structure without launching a session. Run from the repo root to
validate the marketplace manifest (`.claude-plugin/marketplace.json`), which
descends into the plugin, or point at the plugin directory to validate it alone:

```sh
claude plugin validate
claude plugin validate . # marketplace + the plugin it lists
claude plugin validate ./plugins/slack # just the slack plugin
```

### Testing in Cursor
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ When one `SKILL.md` references another skill (e.g., to delegate a step instead o
- Add a sentence of prose explaining what the referenced section does and why you're delegating to it.
- Don't use markdown anchor links (`[text](#step-1)`), `@`-include syntax (`@path/to/SKILL.md`), or bare file paths — none are idiomatic in installed skills, and `@`-includes force-load context.

See `skills/create-slack-app/SKILL.md` Step 1a for an example.
See `plugins/slack/skills/create-slack-app/SKILL.md` Step 1a for an example.

## Testing

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ The plugin connects your AI tool to Slack's hosted [MCP server][slack-mcp-docs]:

Six skills load on demand to handle messaging tasks and developer workflows:

- [`slack:slack-messaging`](skills/slack-messaging/SKILL.md) - composing well-formatted, effective Slack messages
- [`slack:slack-search`](skills/slack-search/SKILL.md) - finding messages, files, channels, and people
- [`slack:slack-api`](skills/slack-api/SKILL.md) - discovering and calling Slack Web API methods
- [`slack:slack-cli`](skills/slack-cli/SKILL.md) - using the [Slack CLI][slack-cli] to create, run, and manage apps
- [`slack:create-slack-app`](skills/create-slack-app/SKILL.md) - building a Slack app or agent with the CLI and [Bolt][bolt]
- [`slack:block-kit`](skills/block-kit/SKILL.md) - building and validating [Block Kit][block-kit] layouts
- [`slack:slack-messaging`](plugins/slack/skills/slack-messaging/SKILL.md) - composing well-formatted, effective Slack messages
- [`slack:slack-search`](plugins/slack/skills/slack-search/SKILL.md) - finding messages, files, channels, and people
- [`slack:slack-api`](plugins/slack/skills/slack-api/SKILL.md) - discovering and calling Slack Web API methods
- [`slack:slack-cli`](plugins/slack/skills/slack-cli/SKILL.md) - using the [Slack CLI][slack-cli] to create, run, and manage apps
- [`slack:create-slack-app`](plugins/slack/skills/create-slack-app/SKILL.md) - building a Slack app or agent with the CLI and [Bolt][bolt]
- [`slack:block-kit`](plugins/slack/skills/block-kit/SKILL.md) - building and validating [Block Kit][block-kit] layouts

### Commands

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"name": "Slack",
"url": "https://slack.com"
},
"homepage": "https://github.com/slackapi/slack-mcp-cursor-plugin",
"homepage": "https://github.com/slackapi/slack-mcp-plugin",
"license": "MIT"
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ tools = [

# This project is installed only to run the test suite; the importable package
# is `tests`. Scope discovery to it so setuptools' flat-layout autodiscovery
# doesn't trip over the sibling `skills/` and `commands/` directories.
# doesn't trip over the sibling `plugins/` and `scripts/` directories.
[tool.setuptools.packages.find]
include = ["tests*"]

Expand All @@ -49,7 +49,7 @@ indent-style = "space"

# rumdl: markdownlint-compatible linter for the plugin's authored markdown.
[tool.rumdl]
include = ["skills/**/*.md", "commands/*.md", "README.md", "AGENTS.md"]
include = ["plugins/slack/skills/**/*.md", "plugins/slack/commands/*.md", "README.md", "AGENTS.md"]
exclude = [".venv", "node_modules", ".changeset"]
respect-gitignore = true
# Disable rules that conflict with the skill-authoring style:
Expand Down
13 changes: 8 additions & 5 deletions scripts/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
}

REPO_ROOT = Path(__file__).resolve().parent.parent
# The plugin lives under plugins/slack/; glob and re-base against it so the installed
# layout is flattened (no plugins/slack/ prefix), matching what Cursor expects at its root.
PLUGIN_ROOT = REPO_ROOT / "plugins" / "slack"

CLAUDE_HOME_DIR = Path.home() / ".claude"
CLAUDE_INSTALLED_PLUGINS_PATH = CLAUDE_HOME_DIR / "plugins" / "installed_plugins.json"
Expand All @@ -42,7 +45,7 @@ def get_target_path(plugin_key: str) -> Path:
def plugin_name() -> str:
"""Read the plugin name from the Cursor plugin file so renames are picked up."""
cursor_plugin = json.loads(
(REPO_ROOT / ".cursor-plugin" / "plugin.json").read_text()
(PLUGIN_ROOT / ".cursor-plugin" / "plugin.json").read_text()
)
name: str = cursor_plugin["name"]
return name
Expand All @@ -52,10 +55,10 @@ def plugin_files() -> set[Path]:
included = {
path
for pattern in INCLUDE
for path in REPO_ROOT.glob(pattern)
for path in PLUGIN_ROOT.glob(pattern)
if path.is_file()
}
excluded = {path for pattern in EXCLUDE for path in REPO_ROOT.glob(pattern)}
excluded = {path for pattern in EXCLUDE for path in PLUGIN_ROOT.glob(pattern)}
return included - excluded


Expand All @@ -78,12 +81,12 @@ def install() -> None:

files = plugin_files()
if not files:
logger.warning(f"No plugin files found under {REPO_ROOT}; nothing to install")
logger.warning(f"No plugin files found under {PLUGIN_ROOT}; nothing to install")
return

shutil.rmtree(target, ignore_errors=True)
for source in files:
dest = target / source.relative_to(REPO_ROOT)
dest = target / source.relative_to(PLUGIN_ROOT)
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, dest)
logger.info(f"Copied {len(files)} plugin files to {target}")
Expand Down
8 changes: 5 additions & 3 deletions scripts/sync_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
logger = logging.getLogger(Path(__file__).stem)

REPO_ROOT = Path(__file__).resolve().parent.parent
PLUGIN_ROOT = REPO_ROOT / "plugins" / "slack"

# package.json (committed at the repo root) is the version source of truth; changesets
# bumps it, then this script synchronizes the version out to everywhere else its defined.
# bumps it, then this script synchronizes the version out to the plugin manifests under
# plugins/slack/.
PACKAGE_JSON_PATH = REPO_ROOT / "package.json"
CLAUDE_PLUGIN_PATH = REPO_ROOT / ".claude-plugin" / "plugin.json"
CURSOR_PLUGIN_PATH = REPO_ROOT / ".cursor-plugin" / "plugin.json"
CLAUDE_PLUGIN_PATH = PLUGIN_ROOT / ".claude-plugin" / "plugin.json"
CURSOR_PLUGIN_PATH = PLUGIN_ROOT / ".cursor-plugin" / "plugin.json"


def read_version(package_path: Path) -> str:
Expand Down
10 changes: 7 additions & 3 deletions tests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@

from dotenv import load_dotenv

# Repo root holds the tooling; the plugin itself lives under plugins/slack/.
REPO_ROOT = Path(__file__).parent.parent
PLUGIN_ROOT = REPO_ROOT / "plugins" / "slack"

# Load .env from the repo root (if present) before reading the environment
load_dotenv(Path(__file__).parent.parent / ".env")
load_dotenv(REPO_ROOT / ".env")


def get_gemini_api_key_pool(environ: Mapping[str, str]) -> list[str]:
return [environ[name].strip() for name in environ if name.startswith("GEMINI_API_KEY") and environ[name].strip()]


# Filesystem
SKILLS_ROOT = Path(__file__).parent.parent / "skills"
SKILLS_ROOT = PLUGIN_ROOT / "skills"

# Plugin namespace (single source of truth: the plugin manifest)
PLUGIN_MANIFEST = Path(__file__).parent.parent / ".claude-plugin" / "plugin.json"
PLUGIN_MANIFEST = PLUGIN_ROOT / ".claude-plugin" / "plugin.json"
PLUGIN_NAME = json.loads(PLUGIN_MANIFEST.read_text())["name"]

# Skill inventory (single source of truth)
Expand Down