diff --git a/CLEANUP_NOTES.md b/CLEANUP_NOTES.md new file mode 100644 index 0000000..f08641f --- /dev/null +++ b/CLEANUP_NOTES.md @@ -0,0 +1,47 @@ +## Cleanup Notes ~ May 4, 2026 + +This repository currently mixes multiple partially finished workstreams. The +goal is to recover to a clean, reviewable git state with focused commits so +normal development can continue. + +## Scope and constraints + +- `TODO.md` is long-horizon context and is out of scope for this cleanup pass. +- `redMCP/` is actively used on this machine; do not delete files in that tree + and do not stop running `redMCP` processes during cleanup. +- `redMCP/startProd.sh` is a local convenience script and is intentionally not a + project artifact for this cleanup. Ignore it. +- Use `plugins/redmine_contacts_helpdesk/LOCAL_CHANGELOG.md` as a primary anchor + for reconstructing intent and grouping related changes. + +## Recovered change groups + +The current dirty tree appears to contain these distinct units: + +1. Helpdesk issue API `include=helpdesk` patch and docs/manifest. +2. Post-import automation and validator/worker hardening. +3. Semantic index service, deployment assets, tests, and runbooks. +4. redMCP feature expansion (HTTP handler/server, client/dispatcher updates, + tests, docs). +5. Skill metadata/docs under `skills/redmine-communicator/`. + +## Working checklist + +- [x] Inventory all modified and untracked files. +- [x] Identify likely project groupings for clean commits. +- [x] Confirm `LOCAL_CHANGELOG.md` aligns with Helpdesk API patch files. +- [ ] Stage and commit Helpdesk API patch as a focused unit. +- [ ] Stage and commit post-import automation as a focused unit. +- [ ] Stage and commit semantic index files as a focused unit. +- [ ] Stage and commit redMCP feature updates as a focused unit. +- [ ] Stage and commit redmine-communicator skill files (optional split). +- [ ] Run targeted syntax/tests for each committed unit. +- [ ] Confirm final worktree state and note any intentionally uncommitted files. + +## Notes to keep in mind + +- Do not commit secrets (`.env`, tokens, credentials). +- `semantic_index/search.sh.md` looks like conversational scratch text; treat as + optional/non-essential unless deliberately kept. +- If a file belongs to multiple units, prefer smallest safe unit first and + document rationale in commit messages. diff --git a/skills/redmine-communicator/SKILL.md b/skills/redmine-communicator/SKILL.md new file mode 100644 index 0000000..ab6ea79 --- /dev/null +++ b/skills/redmine-communicator/SKILL.md @@ -0,0 +1,78 @@ +--- +name: redmine-communicator +description: Use when an agent needs to install, configure, or operate the redMCP MCP server to communicate with Redmine, including Helpdesk-aware issue reads, safe issue updates, attachment handling, and explicit customer-visible Helpdesk responses. +--- + +# Redmine Communicator + +## Overview + +Use this skill to connect an agent to Redmine through `redMCP`, a PHP MCP server +that wraps Redmine's REST API and LDR's Helpdesk-aware extensions. + +Use `redMCP` instead of ad hoc HTTP calls when the task involves Redmine issues, +projects, users, attachments, project categories, issue relations, or Helpdesk +customer communications. + +## Setup Workflow + +1. Install or stage `redMCP` from this repository: + + ```sh + python3 skills/redmine-communicator/scripts/setup_redmcp.py \ + --redmine-url http://redmine.example.test \ + --redmine-api-key "$REDMINE_API_KEY" + ``` + + The default is dry-run. Add `--apply` to copy files and write `.env`. + +2. Configure the MCP client with the printed stdio config. The command points to: + + ```text + /bin/redmcp-server.php + ``` + +3. Verify the server from the agent or client by listing tools. If using the + Streamable HTTP transport, generate and configure `MCP_SERVER_TOKEN`. + +4. Read [references/redmcp-tools.md](references/redmcp-tools.md) before making + customer-visible changes or using less common tools. + +## Operating Rules + +- Prefer read-only tools first: list/search projects, issues, users, categories, + memberships, and Helpdesk context before changing anything. +- For Helpdesk-backed issues, use `redmine_issue_with_helpdesk` instead of a + plain issue read when customer identity or email context matters. +- `redmine_update_issue` is internal-note safe by default. It does **not** send + Helpdesk customer email unless `options.send_helpdesk_email=true` is passed. +- Use `redmine_send_helpdesk_response` only when the user explicitly wants a + customer-visible Helpdesk email. +- Do not invent Redmine project identifiers, tracker ids, category ids, or user + ids. Discover them with redMCP tools first. +- Do not put Redmine API keys, MCP bearer tokens, passwords, or customer secrets + in logs, committed files, or final answers. + +## Common Tool Choices + +- Find work: `redmine_list_issues`, `redmine_search`, `redmine_search_issues`. +- Read one issue: `redmine_get_issue`; use `redmine_issue_with_helpdesk` for + Helpdesk/customer context. +- Internal update: `redmine_update_issue` with fields only. +- Customer reply: `redmine_send_helpdesk_response`, or + `redmine_update_issue` with `options.send_helpdesk_email=true`. +- Attachments: `redmine_upload_attachment`, then include returned upload token + in issue create/update; `redmine_download_attachment` only to safe local paths. +- Structure: issue relation, parent/child, project category, project membership, + project, and user tools are available through MCP. + +## Troubleshooting + +- If `redmcp-server.php` fails immediately, check that `.env` contains + `REDMINE_URL` and `REDMINE_API_KEY`. +- If PHP autoloading fails, run `composer install` in the `redMCP` install + directory, or install from a package that includes `vendor/`. +- If HTTP transport is used, `MCP_SERVER_TOKEN` is required and clients must send + `Authorization: Bearer `. +- Debug logging can include customer text and issue notes. Enable it only for + local troubleshooting and store logs somewhere private. diff --git a/skills/redmine-communicator/agents/openai.yaml b/skills/redmine-communicator/agents/openai.yaml new file mode 100644 index 0000000..fa61bc6 --- /dev/null +++ b/skills/redmine-communicator/agents/openai.yaml @@ -0,0 +1,12 @@ +interface: + display_name: "Redmine Communicator" + short_description: "Connect agents to Redmine through redMCP" + default_prompt: "Use $redmine-communicator to connect to Redmine, inspect issues, and make safe Helpdesk-aware updates." +dependencies: + tools: + - type: "mcp" + value: "redmcp" + description: "Local redMCP server exposing Redmine and Helpdesk-aware tools." + transport: "stdio" +policy: + allow_implicit_invocation: true diff --git a/skills/redmine-communicator/references/redmcp-tools.md b/skills/redmine-communicator/references/redmcp-tools.md new file mode 100644 index 0000000..9a73393 --- /dev/null +++ b/skills/redmine-communicator/references/redmcp-tools.md @@ -0,0 +1,125 @@ +# redMCP Tool Reference + +Use this reference after the `redmine-communicator` skill triggers and the task +requires specific tool selection or setup details. + +## Runtime + +Required environment: + +```text +REDMINE_URL=http://redmine.example.test +REDMINE_API_KEY=... +``` + +For Streamable HTTP MCP: + +```text +MCP_SERVER_TOKEN=... +``` + +Stdio server: + +```sh +redMCP/bin/redmcp-server.php +``` + +HTTP server: + +```sh +MCP_SERVER_TOKEN=... redMCP/bin/redmcp-http-server.php --host 0.0.0.0 --port 8765 +``` + +HTTP endpoint defaults to `/mcp` and requires `Authorization: Bearer `. + +## Read Tools + +- `redmine_list_projects`: list projects. +- `redmine_get_project`: fetch one project by id or identifier. +- `redmine_list_project_memberships`: users/groups and roles for a project. +- `redmine_list_users`, `redmine_get_user`: user discovery. +- `redmine_list_issues`: structured issue filters with friendly fields like + `project_id`, `status`, `updated`, `created`, `sort`, `limit`, and `page`. +- `redmine_search`, `redmine_search_issues`: Redmine native text search. +- `redmine_get_issue`: plain issue read. +- `redmine_issue_with_helpdesk`: issue plus Helpdesk ticket/contact/messages. +- `redmine_list_project_issue_categories`, `redmine_get_issue_category`. +- `redmine_get_attachment`. + +## Write Tools + +- `redmine_create_issue`: create an issue. +- `redmine_update_issue`: update fields or add an internal note. Helpdesk email + is opt-in with `options.send_helpdesk_email=true`. +- `redmine_send_helpdesk_response`: send a customer-visible Helpdesk email. +- `redmine_create_issue_relation`, `redmine_remove_issue_relation`. +- `redmine_set_issue_parent`, `redmine_clear_issue_parent`. +- `redmine_create_issue_category`, `redmine_update_issue_category`. +- `redmine_upload_attachment`, `redmine_download_attachment`, + `redmine_update_attachment`. + +## Safety Notes + +- Customer-visible email requires explicit intent. Prefer internal notes unless + the user asks to email the customer. +- Deletion tools for issues, projects, users, categories, and attachments are + intentionally not exposed. Relation removal only unlinks the relationship. +- For Helpdesk workflows, read with `redmine_issue_with_helpdesk` before + replying so the agent sees customer/contact context. +- For file uploads, use `redmine_upload_attachment` with a path, base64 content, + data URL, or file envelope. Use data/file inputs for PDFs and non-image files. +- `redmine_download_attachment` requires an explicit path under `/tmp` or the + repository tree and limits optional base64 response size. + +## Example MCP Client Config + +```json +{ + "mcpServers": { + "redmcp": { + "command": "/path/to/redMCP/bin/redmcp-server.php" + } + } +} +``` + +## Example Calls + +Read Helpdesk-aware issue context: + +```json +{ + "name": "redmine_issue_with_helpdesk", + "arguments": { + "issue_id": 39858, + "include": ["journals", "attachments"], + "message_limit": 100 + } +} +``` + +Internal note: + +```json +{ + "name": "redmine_update_issue", + "arguments": { + "issue_id": 39858, + "fields": { + "notes": "Internal follow-up note." + } + } +} +``` + +Customer-visible Helpdesk reply: + +```json +{ + "name": "redmine_send_helpdesk_response", + "arguments": { + "issue_id": 39858, + "content": "Customer-visible response text." + } +} +``` diff --git a/skills/redmine-communicator/scripts/setup_redmcp.py b/skills/redmine-communicator/scripts/setup_redmcp.py new file mode 100755 index 0000000..d99f957 --- /dev/null +++ b/skills/redmine-communicator/scripts/setup_redmcp.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Install or stage redMCP for another agent's MCP client.""" + +import argparse +import json +import os +import shutil +import stat +import subprocess +import sys +from pathlib import Path + + +DEFAULT_INSTALL_DIR = Path.home() / ".local" / "share" / "redmcp" + + +def main(): + parser = argparse.ArgumentParser(description="Install redMCP and print MCP client configuration.") + parser.add_argument("--source-redmcp", type=Path, default=repo_root() / "redMCP") + parser.add_argument("--install-dir", type=Path, default=DEFAULT_INSTALL_DIR) + parser.add_argument("--redmine-url", required=True) + parser.add_argument("--redmine-api-key", required=True) + parser.add_argument("--transport", choices=["stdio", "http"], default="stdio") + parser.add_argument("--mcp-server-token", default="") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8765) + parser.add_argument("--apply", action="store_true", help="Copy files and write .env. Default is dry-run.") + args = parser.parse_args() + + if args.transport == "http" and not args.mcp_server_token: + print("error: --mcp-server-token is required for --transport http", file=sys.stderr) + return 2 + + if not args.source_redmcp.is_dir(): + print("error: source redMCP directory not found: {0}".format(args.source_redmcp), file=sys.stderr) + return 1 + + print("mode={0}".format("apply" if args.apply else "dry-run")) + print("source_redmcp={0}".format(args.source_redmcp)) + print("install_dir={0}".format(args.install_dir)) + + if args.apply: + copy_redmcp(args.source_redmcp, args.install_dir) + write_env(args.install_dir / ".env", args) + ensure_executable(args.install_dir / "bin" / "redmcp-server.php") + ensure_executable(args.install_dir / "bin" / "redmcp-http-server.php") + else: + print("would copy redMCP files") + print("would write {0} with REDMINE_URL and REDMINE_API_KEY".format(args.install_dir / ".env")) + + print_runtime_notes(args) + print_mcp_config(args) + return 0 + + +def repo_root(): + return Path(__file__).resolve().parents[3] + + +def copy_redmcp(source, target): + if target.exists(): + shutil.rmtree(str(target)) + ignore = shutil.ignore_patterns(".env", ".cache", "__pycache__", "*.pyc", "*.log") + shutil.copytree(str(source), str(target), ignore=ignore) + + +def write_env(path, args): + lines = [ + "REDMINE_URL={0}".format(args.redmine_url.rstrip("/")), + "REDMINE_API_KEY={0}".format(args.redmine_api_key), + ] + if args.transport == "http": + lines.append("MCP_SERVER_TOKEN={0}".format(args.mcp_server_token)) + path.write_text("\n".join(lines) + "\n") + os.chmod(str(path), 0o600) + + +def ensure_executable(path): + if path.exists(): + mode = path.stat().st_mode + path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def print_runtime_notes(args): + composer = shutil.which("composer") + vendor = args.install_dir / "vendor" / "autoload.php" + if args.apply and not vendor.exists(): + if composer: + print("vendor/autoload.php missing; run: cd {0} && composer install".format(args.install_dir)) + else: + print("vendor/autoload.php missing and composer was not found on PATH") + elif not args.apply: + print("after apply, ensure vendor/autoload.php exists or run composer install in the install dir") + + +def print_mcp_config(args): + if args.transport == "stdio": + config = { + "mcpServers": { + "redmcp": { + "command": str(args.install_dir / "bin" / "redmcp-server.php") + } + } + } + print(json.dumps(config, indent=2)) + return + + command = "{0} --host {1} --port {2}".format( + args.install_dir / "bin" / "redmcp-http-server.php", + args.host, + args.port, + ) + print("start_http_server={0}".format(command)) + print("mcp_url=http://{0}:{1}/mcp".format(args.host, args.port)) + print("authorization=Bearer ") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_redmine_communicator_skill.py b/tests/test_redmine_communicator_skill.py new file mode 100644 index 0000000..52f8c11 --- /dev/null +++ b/tests/test_redmine_communicator_skill.py @@ -0,0 +1,49 @@ +import subprocess +import sys +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SKILL = ROOT / "skills" / "redmine-communicator" +SETUP = SKILL / "scripts" / "setup_redmcp.py" + + +class RedmineCommunicatorSkillTest(unittest.TestCase): + def test_skill_files_exist_and_reference_redmcp_safety_rules(self): + skill_md = (SKILL / "SKILL.md").read_text() + reference = (SKILL / "references" / "redmcp-tools.md").read_text() + + self.assertIn("redmine-communicator", skill_md) + self.assertIn("redMCP", skill_md) + self.assertIn("send_helpdesk_email=true", skill_md) + self.assertIn("redmine_send_helpdesk_response", reference) + self.assertIn("customer-visible", reference) + + def test_setup_script_dry_run_prints_stdio_config(self): + result = subprocess.run( + [ + sys.executable, + str(SETUP), + "--redmine-url", + "http://redmine.example.test", + "--redmine-api-key", + "secret-key", + "--transport", + "stdio", + ], + cwd=ROOT, + universal_newlines=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("mode=dry-run", result.stdout) + self.assertIn("redmcp-server.php", result.stdout) + self.assertNotIn("secret-key", result.stdout) + + +if __name__ == "__main__": + unittest.main()