diff --git a/README.md b/README.md index f9c7d4d..0cd52bb 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,33 @@ All Bearer-authenticated, rate-limited, and (like `create_ticket_api.php`) exemp | `/api/ticket_comment_api.php` | POST | read_write | Add a comment: `{ticket_id, comment_text, markdown_enabled?}` | | `/api/ticket_status_api.php` | POST | read_write | Change/close status (workflow-validated): `{ticket_id, status, comment?}` — `comment` is required for transitions that require one (e.g. → Closed); it is posted as the close reason in the same call | +### MCP server (Claude Code and other MCP clients) +`/mcp` is a remote [MCP](https://modelcontextprotocol.io) server (Streamable HTTP) protected by **OAuth via Authelia** — no API keys. You sign in once in the browser and every action runs **as you**: the same visibility, permissions, Workflow Designer rules, audit log and notifications as the web UI. + +**Connect Claude Code** (one-time): +```bash +claude mcp add --transport http --scope user \ + --client-id tinker-tickets-mcp --callback-port 47823 \ + tinker https://t.lotusguild.org/mcp +claude mcp login tinker # add --no-browser on a headless machine, then paste the redirect URL back +``` +`--client-id` / `--callback-port 47823` are required: Authelia doesn't support dynamic client registration, so Claude Code uses the pre-registered public client `tinker-tickets-mcp`, whose only allowed redirect is `http://localhost:47823/callback`. Access requires the same `admin`/`employee` group as the web UI. Tokens refresh automatically (refresh token lasts 30 days); `claude mcp logout tinker` signs out. + +| Tool | Scope | What it does | +|------|-------|--------------| +| `search_tickets` | `tickets:read` | Search/list tickets you can see (text, status, priority, category, assignee `me`/`unassigned`/username), paginated | +| `get_ticket` | `tickets:read` | One ticket's details + comments | +| `create_ticket` | `tickets:write` | Create a ticket (title, description, priority, category, type, visibility, assignee) | +| `add_comment` | `tickets:write` | Comment (markdown, @mentions, replies) | +| `update_status` | `tickets:write` | Workflow-validated status change; `comment` required when the transition requires one | +| `assign_ticket` | `tickets:write` | Assign / unassign (admin, creator, or current assignee only) | + +How it fits together (see issue #111 for the full design): +- **Authelia** is the authorization server: client `tinker-tickets-mcp`, custom scopes `tickets:read`/`tickets:write`, RS256 JWT access tokens carrying `preferred_username`/`groups`. Needs Authelia **≠ 4.39.21/4.39.22** (RFC 8707 `resource` bug); 4.39.20 or ≥ 4.39.23 are fine. +- **`mcp/server.php`** is the resource server (official `mcp/sdk`, pinned). It validates each token's signature (JWKS), issuer, **audience = this server's URL** (`MCP_RESOURCE_URL`, derived from `APP_DOMAIN`, so a beta token can't be used on prod) and expiry, then maps the user with the same rules as the web login. It serves RFC 9728 metadata at `/.well-known/oauth-protected-resource/mcp`. +- **Composer is used only by the MCP endpoint** (`vendor/` is gitignored and installed by the deploy script with `composer install --no-dev`); nothing else in the app loads it. +- **Reverse proxy:** `/mcp` and `/.well-known/oauth-protected-resource` are exempt from Authelia forward-auth (the token is the credential) and have `Remote-*` headers blanked; `vendor/`, `mcp/` and `composer.*` are never served. + ### User Management & Authentication - **SSO Integration**: Authelia authentication with LLDAP backend - **Role-Based Access**: Admin and standard user roles @@ -355,7 +382,9 @@ tinker_tickets/ │ ├── config.php # Config + .env loading │ └── requirements.php # PHP version/extension requirements (single source of │ # truth for scripts/check_requirements.php + api/health.php) +├── composer.json / composer.lock # MCP endpoint dependencies only (mcp/sdk pinned); vendor/ installed at deploy ├── controllers/ +│ ├── ApiTicketController.php # Partial ticket updates (status/fields/visibility), shared by update_ticket.php + MCP │ ├── CommentController.php # Comment create/edit/delete + notifications │ ├── DashboardController.php # Dashboard with stats + filters │ └── TicketController.php # Ticket CRUD + timeline + visibility @@ -399,11 +428,24 @@ tinker_tickets/ │ ├── 002_fix_collation_consistency.sql # Upgrade-only (already in baseline for fresh installs) │ ├── 003_fk_on_delete_set_null.sql # Upgrade-only (already in baseline for fresh installs) │ ├── 004_fix_ticket_watchers_type.sql # Upgrade-only (already in baseline for fresh installs) +│ ├── 005_attachment_thumbnails.sql # ticket_attachments.thumbnail_filename +│ ├── 006_api_key_visibility_scope.sql # api_keys.see_all_visibility (public-only by default) +│ ├── 007_notification_retry_queue.sql # Queue for failed Matrix webhook retries │ └── migrate.php # CLI migration runner (tracks applied migrations) +├── mcp/ +│ ├── server.php # MCP endpoint (/mcp): OAuth resource server; only file that loads vendor/ +│ └── src/ +│ ├── Auth/ # IdentityMiddleware (token -> user), ToolScopeMiddleware, McpIdentity +│ ├── Tools/ # TicketReadTools, TicketWriteTools +│ └── ToolCatalog.php # Tool registry + which tools need tickets:write ├── scripts/ │ ├── check_requirements.php # Verify PHP extensions/config prerequisites │ └── cleanup_orphan_uploads.php # Delete orphaned upload files past grace period (cron) -├── uploads/ # File attachment storage +├── services/ +│ ├── AssignmentService.php # Assign/unassign (shared by assign_ticket.php + MCP) +│ ├── CommentService.php # Add comment + mentions/notifications (shared by add_comment.php + MCP) +│ └── TicketCreationService.php # Create ticket (shared by TicketController + MCP) +├── uploads/ # File attachment storage (served only via PHP; nginx: internal) │ └── avatars/ # lldap avatar disk cache ├── views/ │ ├── admin/