{"id":"af5193a1-73c2-4b49-89dd-bee39dbd8d96","slug":"open-aims","kind":"mcp","name":"open-aims","summary":"A self-hosted, keyboard-driven issue tracker for managing issues, projects, and cycles. Built as a fast workspace for small teams that want to run their own planning tool on their own infrastructure.","owner":"christianpasinrey","repoUrl":"https://github.com/christianpasinrey/open-aims","license":"MIT","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-04-30T13:33:55.000Z","addedAt":"2026-10-21T04:30:53.582Z","logoUrl":"https://avatars.githubusercontent.com/u/70205361?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["communication","cloud-infra"],"tags":["initiatives","issues","management-system","open-source","projects","agile","cycles","issue-tracker"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-21T04:39:21.043Z","sample":false,"url":"https://conduits.dev/tools/open-aims","language":"PHP","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# AIMS — Advanced Issue Management System\n\nA self-hosted, keyboard-driven issue tracker for managing issues, projects, and cycles. Built as a fast workspace for small teams that want to run their own planning tool on their own infrastructure.\n\n## Stack\n\n- **Laravel 13** + PHP 8.4 — modular monolith (`app/Modules/*`)\n- **Vue 3.5** + **Inertia 2** — single-page client, server-driven routing\n- **Tailwind CSS v4** + **shadcn-vue** + **Reka UI** — design system\n- **Vite** + **Wayfinder** — typed routes generated from Laravel\n- **MySQL** — primary store\n- **Pest / PHPUnit** — backend tests\n\n## Local setup\n\nRequires PHP 8.4, Composer, Node 20+, and MySQL 8.\n\n```bash\n# 1. Install dependencies\ncomposer install\nnpm install\n\n# 2. Configure environment\ncp .env.example .env\nphp artisan key:generate\n\n# 3. Edit .env — at minimum set DB_DATABASE, DB_USERNAME, DB_PASSWORD\n# Create the database, then:\nphp artisan migrate --seed\n\n# 4. Run dev server\ncomposer dev   # runs PHP server, queue worker, Vite, and log tail concurrently\n# OR\nphp artisan serve\nnpm run dev\n```\n\nThe app is now reachable at `http://localhost:8000`.\n\n## Creating users\n\nRegistration is disabled on purpose: there is no register route and `Features::registration()` is not enabled in `config/fortify.php`. Accounts come from two places.\n\n**1. Invitations.** An existing workspace member invites someone by email; the invitee sets their name and password when accepting. This covers normal onboarding, but it cannot help on a fresh install — there is nobody to send the first invitation.\n\n**2. The `user:create` command.** For the first account, and as the escape hatch for anything the invitation flow cannot cover.\n\n```bash\nphp artisan user:create\n```\n\nIt is interactive by design, so the password never lands in shell history. It prompts for name, email and password, then optionally attaches the user to a workspace with a role (`admin`, `member` or `guest`). The workspace step is skipped when no workspace exists yet, which is the case when bootstrapping.\n\nValidation reuses the application's own profile and password rules, so the command cannot create an account the app itself would reject. The user is created with `email_verified_at` already set — with no register route there is no way for them to trigger the verification mail.\n\nAttach the user to a workspace unless you are bootstrapping. `ResolveWorkspace` never binds `current.workspace` for a user with no membership, so an account created without one can log in but lands on a broken app.\n\n## MCP access\n\nAIMS exposes an MCP server so clients such as Claude Desktop can drive the workspace. Clients self-register at `/oauth/register` and then run the standard OAuth authorization code flow at `/oauth/authorize`, served by Laravel Passport.\n\nPassport signs tokens with an RSA keypair that must be generated once per environment. `storage/` is gitignored, so these keys are **not** carried by a deploy: a freshly deployed environment has none until you generate them.\n\n**Run every command below as the system user that owns the site, never as root.** `passport:keys` writes the keys with mode `600` — readable only by their owner — so keys generated by root are unreadable by the PHP-FPM pool, and the OAuth flow fails just as surely as if they did not exist. The same applies to any other artisan command: running one as root leaves root-owned files in `storage/` and `bootstrap/cache` that break the app later.\n\n```bash\nphp artisan passport:keys\n```\n\n```bash\nchmod 600 storage/oauth-private.key storage/oauth-public.key\n```\n\nThen confirm the ownership matches the PHP-FPM user, and fix it if you slipped and used root (on Plesk the group is usually `psacln`):\n\n```bash\nstat -c '%U:%G' storage/oauth-private.key storage/oauth-public.key\n```\n\n```bash\nchown -R SITE_USER:psacln storage bootstrap/cache\n```\n\nVerify the key actually loads, without going through a browser:\n\n```bash\nphp artisan tinker --execute=\"new League\\OAuth2\\Server\\CryptKey('file://'.storage_path('oauth-private.key')); echo 'key OK';\"\n```\n\nRun that check **as the site user too**. As root it prints `key OK` even when the keys are unreadable by PHP-FPM, which makes it look like the problem is elsewhere.\n\nRegenerating the keys (`passport:keys --force`) invalidates every issued token, and connected MCP clients have to authorize again.\n\n### Troubleshooting `LogicException: Invalid key supplied`\n\nA 500 on `/oauth/authorize` with this exception from `league/oauth2-server` means Passport was handed something that is neither a valid PEM nor an existing file. Passport resolves the key like this:\n\n```php\n$key = str_replace('\\\\n', \"\\n\", config(\"passport.{$type}_key\") ?? '');\n\nif (! $key) {\n    $key = 'file://'.Passport::keyPath('oauth-'.$type.'.key');\n}\n```\n\nSo there are two causes. Either `PASSPORT_PRIVATE_KEY` / `PASSPORT_PUBLIC_KEY` are unset and `storage/oauth-private.key` does not exist — generate the keys — or they are set but their newlines are mangled, so the value is not a valid PEM. Fix the env values, or clear them and fall back to key files.\n\nThe exact message narrows it down:\n\n| Message | Meaning |\n|---|---|\n| `Invalid key supplied` | The path does not exist, or the env value is not a PEM |\n| `Key path \"…\" does not exist or is not readable` | The file exists but the PHP process cannot read it — check ownership |\n| `Unable to read key from file …` | The file exists and is readable, but its contents are not a valid key |\n\n## GitHub integration\n\nAIMS supports two GitHub flows:\n\n### 1. OAuth — sign-in and account linking\n\nLets users sign in with GitHub and link their account to surface their PRs in issues.\n\nIn `.env`:\n\n```\nGITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\nGITHUB_REDIRECT=\"${APP_URL}/gh/callback\"\n```\n\nCreate an OAuth App at <https://github.com/settings/developers>:\n\n- Homepage URL: `${APP_URL}`\n- Authorization callback URL: `${APP_URL}/gh/callback`\n\n### 2. GitHub App — webhook ingestion\n\nConnects a repo (or org) so that PR / commit references like `LAM-123` auto-link to the matching AIMS issue.\n\nIn `.env`:\n\n```\nGITHUB_APP_ID=\nGITHUB_APP_NAME=\nGITHUB_APP_WEBHOOK_SECRET=\nGITHUB_APP_PRIVATE_KEY_PATH=storage/keys/github-app.pem\nGITHUB_APP_INSTALL_URL=https://github.com/apps/<your-app-name>/installations/new\n```\n\nCreate the GitHub App at <https://github.com/settings/apps/new>:\n\n- Webhook URL: `${APP_URL}/gh/webhook`\n- Webhook secret: same as `GITHUB_APP_WEBHOOK_SECRET`\n- Permissions: `Pull requests: read`, `Contents: read`, `Issues: read`, `Metadata: read`\n- Subscribe to events: `Pull request`, `Push`, `Issues`\n- Download the private key and save to `storage/keys/github-app.pem`\n\nMap a repository to a workspace via env (one variable per workspace), e.g.:\n\n```\nGITHUB_APP_REPO_LAM=your-org/your-repo\n```\n\nInstall the App on the configured repo using `GITHUB_APP_INSTALL_URL`. PRs and pushes that reference an issue key (e.g. `LAM-42` in title, branch, or commit message) will be auto-linked.\n\n## Keyboard shortcuts\n\nThe workspace is keyboard-first.\n\n| Shortcut | Action |\n|---|---|\n| `Ctrl+K` / `Cmd+K` | Open the search palette |\n| `G` then `I` | Go to issues |\n| `G` then `P` | Go to projects |\n| `G` then `C` | Go to cycles |\n| `G` then `H` | Go home |\n| `C` | Create a new issue (when not typing in a field) |\n| `?` | Show the shortcuts cheat sheet |\n\n## Useful commands\n\n```bash\n# Create a user account (registration is disabled by design)\nphp artisan user:create\n\n# Generate the Passport keypair backing the MCP OAuth flow\nphp artisan passport:keys\n\n# Run tests\nphp artisan test\n\n# Format PHP\nvendor/bin/pint\n\n# Check / fix JS+Vue\nnpm run lint\nnpm run format\nnpm run types:check\n\n# Production build\nnpm run build\n```\n\n## Project layout\n\n```\napp/\n  Console/Commands/             Artisan commands (user:create, ...)\n  Models/                       Eloquent models (User, Workspace, Team, Issue, ...)\n  Modules/\n    Issues/                     Issues domain (controllers, resources, requests)\n    Projects/                   Projects + milestones\n    Cycles/                     Cycle planning + transitions\n    Workspace/                  Settings, members, invitations\n    Integrations/\n      Github/                   OAuth, App service, webhook handler\nresources/js/\n  pages/                        Inertia pages — one per route\n  components/repo/            Atomic issue-tracker components (StatusIcon, ProjectIcon, ...)\n  components/ui/                shadcn-vue primitives\n  composables/                  Reusable logic (keyboard shortcuts, theme, ...)\n  layouts/                      Inertia persistent layouts\nroutes/\n  web.php                       Public + authenticated routes\n  settings.php                  /settings/* routes\n```\n\n## License\n\nReleased under the [MIT License](LICENSE).\n"}