mcp server
open-aims
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.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 21, 2026AIMS — Advanced Issue Management System
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.
Stack
- Laravel 13 + PHP 8.4 — modular monolith (
app/Modules/*) - Vue 3.5 + Inertia 2 — single-page client, server-driven routing
- Tailwind CSS v4 + shadcn-vue + Reka UI — design system
- Vite + Wayfinder — typed routes generated from Laravel
- MySQL — primary store
- Pest / PHPUnit — backend tests
Local setup
Requires PHP 8.4, Composer, Node 20+, and MySQL 8.
# 1. Install dependencies
composer install
npm install
# 2. Configure environment
cp .env.example .env
php artisan key:generate
# 3. Edit .env — at minimum set DB_DATABASE, DB_USERNAME, DB_PASSWORD
# Create the database, then:
php artisan migrate --seed
# 4. Run dev server
composer dev # runs PHP server, queue worker, Vite, and log tail concurrently
# OR
php artisan serve
npm run dev
The app is now reachable at http://localhost:8000.
Creating users
Registration is disabled on purpose: there is no register route and Features::registration() is not enabled in config/fortify.php. Accounts come from two places.
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.
2. The user:create command. For the first account, and as the escape hatch for anything the invitation flow cannot cover.
php artisan user:create
It 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.
Validation 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.
Attach 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.
MCP access
AIMS 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.
Passport 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.
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.
php artisan passport:keys
chmod 600 storage/oauth-private.key storage/oauth-public.key
Then confirm the ownership matches the PHP-FPM user, and fix it if you slipped and used root (on Plesk the group is usually psacln):
stat -c '%U:%G' storage/oauth-private.key storage/oauth-public.key
chown -R SITE_USER:psacln storage bootstrap/cache
Verify the key actually loads, without going through a browser:
php artisan tinker --execute="new League\OAuth2\Server\CryptKey('file://'.storage_path('oauth-private.key')); echo 'key OK';"
Run 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.
Regenerating the keys (passport:keys --force) invalidates every issued token, and connected MCP clients have to authorize again.
Troubleshooting LogicException: Invalid key supplied
A 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:
$key = str_replace('\\n', "\n", config("passport.{$type}_key") ?? '');
if (! $key) {
$key = 'file://'.Passport::keyPath('oauth-'.$type.'.key');
}
So 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.
The exact message narrows it down:
| Message | Meaning |
|---|---|
Invalid key supplied | The path does not exist, or the env value is not a PEM |
Key path "…" does not exist or is not readable | The file exists but the PHP process cannot read it — check ownership |
Unable to read key from file … | The file exists and is readable, but its contents are not a valid key |
GitHub integration
AIMS supports two GitHub flows:
1. OAuth — sign-in and account linking
Lets users sign in with GitHub and link their account to surface their PRs in issues.
In .env:
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT="${APP_URL}/gh/callback"
Create an OAuth App at https://github.com/settings/developers:
- Homepage URL:
${APP_URL} - Authorization callback URL:
${APP_URL}/gh/callback
2. GitHub App — webhook ingestion
Connects a repo (or org) so that PR / commit references like LAM-123 auto-link to the matching AIMS issue.
In .env:
GITHUB_APP_ID=
GITHUB_APP_NAME=
GITHUB_APP_WEBHOOK_SECRET=
GITHUB_APP_PRIVATE_KEY_PATH=storage/keys/github-app.pem
GITHUB_APP_INSTALL_URL=https://github.com/apps/<your-app-name>/installations/new
Create the GitHub App at https://github.com/settings/apps/new:
- Webhook URL:
${APP_URL}/gh/webhook - Webhook secret: same as
GITHUB_APP_WEBHOOK_SECRET - Permissions:
Pull requests: read,Contents: read,Issues: read,Metadata: read - Subscribe to events:
Pull request,Push,Issues - Download the private key and save to
storage/keys/github-app.pem
Map a repository to a workspace via env (one variable per workspace), e.g.:
GITHUB_APP_REPO_LAM=your-org/your-repo
Install 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.
Keyboard shortcuts
The workspace is keyboard-first.
| Shortcut | Action |
|---|---|
Ctrl+K / Cmd+K | Open the search palette |
G then I | Go to issues |
G then P | Go to projects |
G then C | Go to cycles |
G then H | Go home |
C | Create a new issue (when not typing in a field) |
? | Show the shortcuts cheat sheet |
Useful commands
# Create a user account (registration is disabled by design)
php artisan user:create
# Generate the Passport keypair backing the MCP OAuth flow
php artisan passport:keys
# Run tests
php artisan test
# Format PHP
vendor/bin/pint
# Check / fix JS+Vue
npm run lint
npm run format
npm run types:check
# Production build
npm run build
Project layout
app/
Console/Commands/ Artisan commands (user:create, ...)
Models/ Eloquent models (User, Workspace, Team, Issue, ...)
Modules/
Issues/ Issues domain (controllers, resources, requests)
Projects/ Projects + milestones
Cycles/ Cycle planning + transitions
Workspace/ Settings, members, invitations
Integrations/
Github/ OAuth, App service, webhook handler
resources/js/
pages/ Inertia pages — one per route
components/repo/ Atomic issue-tracker components (StatusIcon, ProjectIcon, ...)
components/ui/ shadcn-vue primitives
composables/ Reusable logic (keyboard shortcuts, theme, ...)
layouts/ Inertia persistent layouts
routes/
web.php Public + authenticated routes
settings.php /settings/* routes
License
Released under the MIT License.