Kantoor
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Kantoorwhat is everyone working on right now?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Kantoor
Kantoor is Dutch for "office". It is a virtual office where your Claude agents work as colleagues at desk islands. Every project or client gets its own island. You see live who is busy, who is waiting for you, who is stuck and who is handing work to a colleague. You answer questions from agents in the office itself, and you can keep the office running full screen on a TV.

Agents come in two ways:
Claude (web, desktop, mobile), skills, scheduled tasks and cloud sessions report through an MCP connector.
Claude Code reports through hooks: every session automatically becomes a colleague at the island of the repository you are working in.
Kantoor is a Laravel app you host yourself. Each installation belongs to one person or one team: everyone with an account sees the whole office.
Contents
Related MCP server: TaskFlow MCP Server
Requirements
PHP 8.4 or higher with the
sqlite3(orpdo_mysql),mbstring,openssl,curlandzipextensionsComposer
For a server: a (sub)domain with HTTPS, Nginx, and a way to keep a process running in the background (Supervisor, systemd or Laravel Forge)
A Claude plan that lets you add custom connectors, and/or Claude Code
You don't need Node.js: the CSS and JavaScript are ready to use in public/.
Kantoor uses SQLite by default, which is plenty. If you prefer MySQL or PostgreSQL, fill in the DB_* lines in .env.
Try it locally first
git clone https://github.com/SERFF/kantoor-public.git kantoor
cd kantoor
composer install
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrate
php artisan passport:keysFor local use, set these in .env:
APP_URL=http://127.0.0.1:8000
KANTOOR_WS_HOST=127.0.0.1
KANTOOR_WS_PORT=8080
KANTOOR_WS_SCHEME=http
REVERB_APP_ID=kantoor
REVERB_APP_KEY=local-key
REVERB_APP_SECRET=local-secretCreate an account:
php artisan kantoor:invite you@example.comThis prints a link that is valid for 30 minutes. Open it and choose a password (at least 12 characters).
Then start these in three terminals:
php artisan serve # the app on http://127.0.0.1:8000
php artisan reverb:start # live updates (websockets) on port 8080
php artisan kantoor:demo # fills the office with demo colleagues for 5 minutesOpen http://127.0.0.1:8000, log in and watch. Remove the demo colleagues with php artisan kantoor:demo --clear.
A local installation is great for a look around, but Claude on the web cannot reach
127.0.0.1. For the connector you need a server with a public HTTPS address. Claude Code hooks do work locally.
Install on a server
The examples use kantoor.example.com. Replace it with your own domain everywhere.
1. Domain and site
Point the domain at your server and create a site with the public/ folder as document root. Request an HTTPS certificate (for example Let's Encrypt). HTTPS is required: the Claude connector only works over HTTPS.
With Laravel Forge: create a new Laravel site, connect your fork of this repository, choose PHP 8.4 or higher and request a Let's Encrypt certificate.
2. Code and .env
git clone https://github.com/SERFF/kantoor-public.git /var/www/kantoor
cd /var/www/kantoor
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
touch database/database.sqliteFill in at least the following in .env:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://kantoor.example.com
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=pick-a-random-string
REVERB_APP_KEY=pick-a-random-string
REVERB_APP_SECRET=pick-a-long-random-string
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8080
KANTOOR_WS_HOST=kantoor.example.com
KANTOOR_WS_PORT=443
KANTOOR_WS_SCHEME=httpsYou can generate random strings with openssl rand -hex 20.
How the traffic flows: the app talks to Reverb internally on 127.0.0.1:8080, and the browser opens a secure websocket through Nginx to wss://kantoor.example.com/app.
With Forge you enter these values on the site's Environment page.
3. Database and keys
php artisan migrate --force
php artisan passport:keys
php artisan optimizepassport:keys creates the keys the connector uses to log in (storage/oauth-*.key). Do this once and keep the files on later deploys.
Make sure the web server can write to storage/, bootstrap/cache/ and database/ (because of SQLite).
4. Reverb (live updates)
Reverb must always be running. With Forge: enable the Reverb integration on the site, with host kantoor.example.com and port 8080. Forge then creates the background process and the Nginx proxy, and this step is done.
Without Forge, create a Supervisor program, for example /etc/supervisor/conf.d/kantoor-reverb.conf:
[program:kantoor-reverb]
command=php /var/www/kantoor/artisan reverb:start --host=127.0.0.1 --port=8080
directory=/var/www/kantoor
user=www-data
autostart=true
autorestart=true
stopwaitsecs=3600sudo supervisorctl reread && sudo supervisorctl updateAnd add this location to the server block of your Nginx config:
location /app {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://127.0.0.1:8080;
}sudo nginx -t && sudo systemctl reload nginx5. Scheduler
Every minute, kantoor:check decides who is stuck, who is done and who can go home. Enable the Laravel scheduler for it.
With Forge: server → Scheduler → new job php /home/forge/kantoor.example.com/artisan schedule:run, every minute.
Without Forge: run sudo crontab -u www-data -e and add:
* * * * * cd /var/www/kantoor && php artisan schedule:run >> /dev/null 2>&1You don't need a queue worker.
6. Your account
php artisan kantoor:invite you@example.com --name="Your name"Open the link it prints (valid for 30 minutes) and choose a password. To give a colleague or housemate access, do the same with their email address. There is no public sign-up. Forgot your password? Run kantoor:invite again for the same address.
7. Check
https://kantoor.example.comshows the login screen, and after logging in the office with the live indicator in the top right.https://kantoor.example.com/.well-known/oauth-authorization-serverreturns JSON. That means the connector is ready to be connected.
Deploy script (Forge)
cd $FORGE_SITE_PATH
git pull origin $FORGE_SITE_BRANCH
$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader
$FORGE_PHP artisan migrate --force
$FORGE_PHP artisan optimize
$FORGE_PHP artisan reverb:restartConnect Claude
In Claude, go to Settings → Connectors → Add custom connector.
Enter
https://kantoor.example.com/mcpas the URL.You are sent to your office. Log in and click Allow.
Claude then gets five tools:
Tool | What it does |
| Reports |
| Shows work moving to a colleague. |
| Fetches your answer to a question you answered in the office. |
| Who is doing what, for example for "who is waiting for me?". |
| End-of-day recap: what was delivered per project and what is still open. |
| Weekly recap: deliveries, sessions, prompts, lines edited, the busiest day and the totals per project. |
The connector tells Claude by itself when to report. If you want Claude to report in every chat, add a line to your personal preferences (Settings → Profile), for example:
If the Kantoor connector is available, report work that is more than a quick question to the office:
working when you start, waiting when you need me, done with a one-line result, error when you get stuck.
Use a fixed agent name per project, and the name of the project or client as project.Subagents are reported by the agent that starts them, each with its own name. Names ending in -researcher, -builder, -reviewer or -tester (optionally with a number, like webshop-builder-2) are placed next to the agent named by the first part. For other names, pass parent to report_status.
Connect Claude Code
On the server, create one token per computer:
php artisan kantoor:hook-token "Laptop"This prints the token and an install command like this one. Run it on that computer:
curl -fsSL https://kantoor.example.com/install.sh | bash -s -- kh_...The script checks the token, backs up ~/.claude/settings.json, shows you the change and asks before writing it. It puts kantoor-hook.sh and kantoor-usage.sh in ~/.claude/hooks/, stores the address and token in ~/.claude/kantoor.env (readable only by you), replaces earlier Kantoor hooks and keeps your own status line working. It needs jq and curl. You can run it again at any time; remove everything with ... | bash -s -- --remove.
From then on every session becomes a colleague at the island of the repository you are working in: the folder directly under Code/, Herd/, Projects/ or Sites/ in your home folder (change this with KANTOOR_CODE_ROOTS). You see what the session is doing (editing a file, running the tests), when it is waiting for permission and when it is done. Subagents become separate colleagues next to the session.
Every edit shows the file name and how many lines were added and removed ("Editing Planning.php +12 −3").
Permission requests and questions in the office. When Claude Code asks for permission (a command, an edit) or asks you something with AskUserQuestion, it shows up in the office with buttons: Allow, Always allow (when Claude Code suggests a rule), Deny or Answer in the terminal. If you type something yourself, the office denies the request with your text as the explanation for Claude. Several questions come one at a time. The hook waits for your answer and passes it on; every answer only applies to that one request.
By default this only goes through the office while you are away from your Mac (two minutes without mouse or keyboard). When you are at your desk, Claude Code asks you as usual. If you come back while the question is in the office, it moves back to Claude Code, and so it does after 30 minutes without an answer. Settings in ~/.claude/kantoor.env:
Setting | What |
|
|
| seconds without mouse or keyboard before you count as away (120) |
| how long the hook waits for your answer, in seconds (1800) |
| do not send file names |
| do not send the command or path with a permission request |
Away detection uses macOS idle time; on other systems you count as away all the time, so set KANTOOR_APPROVE to what you prefer.
The hooks fail silently and wait only briefly. If the office is unreachable, Claude Code doesn't notice. Revoke a token with:
php artisan kantoor:hook-token "Laptop" --revokeWeekly usage on the whiteboard (optional)
The whiteboard in the office can show how much of your weekly Claude Code usage limit is used and how much is left. Claude Code passes these numbers to its status line, so a small wrapper script forwards them to the office.
The install script from Connect Claude Code already does this. To do it by hand:
Install
jqif you don't have it (brew install jqorsudo apt install jq).Copy the script from this repository and make it executable:
mkdir -p ~/.claude/hooks cp resources/statusline/kantoor-usage.sh ~/.claude/hooks/ chmod +x ~/.claude/hooks/kantoor-usage.shAdd a status line to
~/.claude/settings.json:"statusLine": { "type": "command", "command": "KANTOOR_URL=https://kantoor.example.com ~/.claude/hooks/kantoor-usage.sh" }Already have a status line of your own? Put its command in
KANTOOR_STATUSLINEand the script passes everything through to it:"command": "KANTOOR_URL=https://kantoor.example.com KANTOOR_STATUSLINE='your-statusline-command' ~/.claude/hooks/kantoor-usage.sh"
The script uses the hook token from your settings.json (or KANTOOR_TOKEN if you set it), only sends when the numbers change or every 10 minutes, and never blocks the status line.
From the Claude desktop app (macOS)
The status line only runs in the terminal. If you mostly use the Claude desktop app, the app itself keeps the same numbers in ~/Library/Application Support/Claude/plan-usage-history.json. On a Mac with the desktop app, the install script also installs a small launchd agent (kantoor-desktop-usage.sh) that sends every new measurement to the office, together with the history of the last four weeks. The app only writes a measurement now and then (mostly while you use it), so the board shows when the numbers were measured. By hand:
cp resources/statusline/kantoor-desktop-usage.sh ~/.claude/hooks/
bash ~/.claude/hooks/kantoor-desktop-usage.sh --installIf you also run the Claude Usage app, the agent also reads the history that app keeps: the 5-hour window every 10 minutes and the week every 2 hours. For each window the newest measurement from either source goes to the office, and the whiteboard then shows the 5-hour window next to its title.
Remove it with --remove. When both the status line and the desktop app report, the newest measurement wins. A reset time from the status line beats the one estimated from the desktop app (the board marks an estimated reset with ±).
Forecast
The triangle on the gauge shows where the tank ends up at the reset if you keep the same pace. The office learns your pace per part of the day (night, work hours, evening, weekend) from the measurements of the last four weeks and adds the pace of the last few hours. If the tank runs empty before the reset, the triangle turns red and the bottom of the board says when. Parts of the day follow KANTOOR_TIMEZONE.
Weekly recap and sounds
Under "This week" the office shows what happened since Monday: deliveries, Claude Code sessions, prompts, edits with the number of lines, the busiest day, the project with the most work and how often you answered in the office. On Monday morning until noon it shows last week instead, also on the TV. week_summary gives Claude the same recap.
The office plays a short sound when someone is waiting for you, gets stuck or is done. Turn it off with the Sound button at the top. Browsers only play sound after a first click or key press; on the TV that is the click for full screen.
Reporting from a skill
In a skill of your own, state at the top of SKILL.md when to report, for example:
## Office
Report to the office with `report_status` (agent: `weekly-report`, project: `Internal`, role: `Writer`):
- `working` when you start;
- `waiting` when something is ready to review or you have a question, saying what you need from me;
- `done` at the end with a one-line result, or `error` with the reason.Always use the same name for agent, so it stays the same colleague.
The office on a TV
At https://kantoor.example.com/tv you see the TV view when you are logged in. For a TV you don't want to log in on, create a view-only link:
php artisan kantoor:tv-link "Living room"Open the link in the TV's browser. Anyone with the link can watch (not change anything), so don't share it further. Revoke it with --revoke.
Ringing the phone on errors (optional)
Kantoor can make the phone on your desk ring when your error monitor reports a new error. Create a webhook URL:
php artisan kantoor:alert-link "Monitoring"The URL is shown only once. Your monitor sends a POST with JSON in this format:
{
"action": "new_issue",
"project": { "name": "Webshop", "slug": "webshop" },
"issue": {
"id": 42,
"short_id": "WEBSHOP-42",
"level": "error",
"times_seen": 3,
"url": "https://monitor.example.com/issues/42"
}
}action is new_issue, regression, frequency or test. Only action, project.name and issue are required. If your monitor can't send this format itself, put a small translation step in between (for example in n8n, Zapier or a script of your own).
Without a token in the URL. If your monitor runs on a server with a fixed IP address, you can also use https://kantoor.example.com/api/hooks/alerts without a token. Only the addresses in KANTOOR_ALERT_IPS (comma-separated) are accepted; with no addresses set, this URL is closed.
An alert appears at the top of the office with a link to the issue and a button to pick up the phone, and the island with the same project name flashes. The same issue doesn't ring again while the phone is already ringing. It stops after an hour (KANTOOR_ALARM_MINUTES) or when you pick up. Kantoor only stores the project, issue code, level, count and link, never the error message itself.
Projects (islands)
A project is created automatically on the first report. Manage projects with kantoor:project:
php artisan kantoor:project # list
php artisan kantoor:project Webshop --match="webshop*,shop-api" # folders that belong to Webshop
php artisan kantoor:project Webshop --merge=webshop-2026 # merge a duplicate island
php artisan kantoor:project Webshop --rename="Web store" # rename
php artisan kantoor:project Internal --position=9 # order of the islandsYou change a colleague's name, role and project in the office itself by clicking on the character.
Settings
Everything lives in .env. After a change, run php artisan optimize.
Variable | Default | What |
| empty | Name at the top of the office and on the TV, for example your company name |
|
| Text on the neon sign on the wall (max. 10 characters) |
|
| Name of the office dog |
|
| Time zone for day and night, Friday drinks and the logbook |
|
| Folders that contain your Claude Code repositories |
|
| Minutes of silence before a working agent counts as stuck |
|
| The same for Claude Code sessions |
|
| Minutes after |
|
| Minutes available without work before someone goes home |
|
| Minutes of silence before a Claude Code session goes home |
|
| Days before a session that went home is forgotten |
|
| Days that deliveries stay in the logbook |
|
| How long the phone keeps ringing |
| empty | IP addresses allowed to post alerts without a token |
|
| Where the connector may return to after logging in |
Life in the office
On a hand-off, the colleague walks a folder over to the desk of whoever gets the work.
Colleagues who have been available for a while sometimes chat in the coffee corner. On Fridays between 16:00 and 19:00 it's Friday drinks.
The office dog lies down next to whoever is stuck, says good morning and begs at the coffee corner.
In the evening the desk lamps come on and the sky changes colour at sunrise and sunset.
At
/logbookyou see per day what was delivered, filtered by project and period. Whoever delivered the most in the past week hangs on the wall as employee of the week.
Updating
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize
php artisan reverb:restartWith Forge, the deploy script does this for you.
Troubleshooting
The live indicator keeps saying it is connecting. The browser can't reach Reverb. Check that Reverb is running (supervisorctl status or Forge → Daemons), that the Nginx location /app is in place, and that KANTOOR_WS_HOST, KANTOOR_WS_PORT=443 and KANTOOR_WS_SCHEME=https are correct. Then run php artisan optimize.
Connecting the connector fails. Open https://kantoor.example.com/.well-known/oauth-authorization-server. No JSON? Then the keys are missing (php artisan passport:keys) or APP_URL is wrong. Also check that MCP_REDIRECT_DOMAINS contains the Claude domains.
Claude Code doesn't show up. Test from that computer with the curl line from settings.json, without -s -o /dev/null. A 401 means a wrong or revoked token. If the session lands on the wrong island, set --match on the project or adjust KANTOOR_CODE_ROOTS.
Colleagues stay "working" forever. The scheduler isn't running. Check the cron job or Forge → Scheduler.
The whiteboard shows no weekly usage. Check that jq is installed, that KANTOOR_URL is set in the status line command, and that the script is executable.
Something else goes wrong. Look in storage/logs/laravel.log.
Privacy and security
The hook script strips everything it doesn't need before sending: no prompts, no file contents, no replies from Claude. Of tool use, only the kind of action, the file name and the number of lines are sent. Only for a permission request that goes through the office, the command or path is sent too, so you know what you are approving; it is deleted as soon as you answer.
Hook tokens, TV links and alert links are stored only as hashes and can be revoked one by one.
Everyone with an account sees everything in the installation. Only give accounts to people who may see everything.
Reports appear on a screen others may see. The connector asks Claude not to put personal data, email addresses or customer content in reports, but check your own skills for this too.
Found a security issue? See SECURITY.md.
Development
php artisan testLicense
MIT, see LICENSE.
Built by Serff Webdevelopment, custom software and AI implementation.
This server cannot be deployed
Maintenance
Related MCP Connectors
- HourtickOAuthcom.hourtick
Time tracking, tasks and team chat for humans and AI agents.
See your team's status, delegate work, follow progress, and approve requests from your AI client.
Shared workspace where Claude, ChatGPT, Codex and Grok share files, tasks, decisions and handoffs.
Project management for AI agents: tasks, docs, decisions and time in one shared team context.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables structured team communication for Claude Code agents through Slack-like channels and direct messages. Supports project isolation, subscription management, and agent notes for sophisticated multi-agent collaboration workflows.13 npm8MIT
- AlicenseAqualityDmaintenanceIntegrates task management capabilities into Claude Desktop, enabling users to create, track, search, and manage tasks with status updates, team assignments, overdue tracking, and automated standup reports.6MIT
- FlicenseNot gradedqualityDmaintenanceProvides tools for AI coding agents to read and write shared team context, including status, pending items, and handoffs. Enables agents to query current project status and claim pending work.2-
- AlicenseAqualityBmaintenanceEnables Claude to interact with System Task projects, teams, and tasks, providing daily briefs, project reports, team load, and risk identification, as well as creating and updating tasks and demands.15MIT