JAVIS Board JAVIS Board Developer docs
← Back to home
Internal · v1

MCP integration guide

JAVIS Board exposes the kanban as a Model Context Protocol server. Self-hosted JAVIS agents — and any other MCP-capable client — connect with a workspace-scoped bearer token and act on behalf of human users.

Endpoint
POST /mcp/board
Transport
HTTP + JSON-RPC 2.0
Auth
Sanctum bearer + acting-user header
28
Tools
3
Resources
1
Server
180d
Audit retention

Overview

The MCP server lives at POST /mcp/board and speaks JSON-RPC 2.0 over HTTP (with SSE for streamed responses). Every call has two identities: the agent (a per-workspace service user authenticated by bearer token) and the actor (the human the agent is acting on behalf of, identified by phone).

Identity model in one sentence

The bearer token says “I am Acme’s JAVIS”. The X-Acting-User-Phone header says “and I’m doing this for User A.” The server checks (a) the bearer is a service user attached to a workspace as agent, (b) the actor phone resolves to a user who is a member of that workspace, and (c) any board/card referenced lives in that same workspace. Then it executes the tool under the actor’s authorization, marking the resulting record with source = agent.

Why MCP and not REST

Tools self-describe. The agent fetches the tool catalog at runtime — names, descriptions, JSON schemas, semantics — instead of having a contract hard-coded into JAVIS code. The same server is also reachable from Claude Desktop, Cursor, or any other MCP-capable client without further work.

Quickstart

Provision a token for one workspace and call the smoke-test tool.

1. Provision JAVIS for a workspace

From the admin panel: Admin → Manage Workspaces. On the workspace row, click Provision JAVIS. The page creates the workspace’s service user (if missing), assigns the ws.agent role, rotates the Sanctum token, and shows the plaintext token once in a persistent notification. Copy it immediately.

Tokens are shown once

Lost a token? Click Provision JAVIS again — the existing token will be revoked and a fresh one issued. Existing card history and audit rows are preserved.

2. Smoke-test the token

Call whoami-tool first — it’s the cheapest probe and needs only the bearer token. If the token is valid, you’ll get back the agent identity and the workspace it’s bound to.

cURL · whoami-toolbearer only · no audit row
# Replace TOKEN and BASE_URL with your values. No phone header needed.
curl -s -X POST "$BASE_URL/mcp/board" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "whoami-tool",
      "arguments": {}
    },
    "id": 1
  }'

A successful response contains agent, workspace, and actor: null (since we didn’t send a phone header).

3. Make a real call on behalf of a user

Once whoami-tool works, add X-Acting-User-Phone for any tool that touches workspace data:

cURL · list-boards-toolcreates an audit row
# PHONE is the E.164 phone of the human you're acting on behalf of.
curl -s -X POST "$BASE_URL/mcp/board" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Acting-User-Phone: $PHONE" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "list-boards-tool",
      "arguments": {}
    },
    "id": 2
  }'

4. Verify the audit row

Every successful or denied call writes a row to agent_call_audit. Inspect it from the admin agent activity feed or directly via SQL:

SQL
SELECT id, agent_user_id, actor_user_id, tool, result, error_code, duration_ms, created_at
  FROM agent_call_audit
 ORDER BY id DESC LIMIT 5;

Authentication

Headers

HeaderPurpose
Authorization: Bearer …Sanctum token issued for the workspace JAVIS service user. Has the ability agent.
Accept: application/jsonRequired to opt into JSON 401 responses (otherwise Laravel redirects).
Content-Type: application/jsonJSON-RPC body.
X-Acting-User-PhoneE.164 phone of the human on whose behalf the agent acts. Required for every mutating tool (creates, updates, archives, deletes, comments, assignments). Optional for the read tools and whoami-tool — sending it just records the actor in the audit row. Auto-provisions a new users row if the phone is unknown (since the agent has is_service=true).
X-Acting-User-NameOptional. Display name to use when auto-provisioning a new user from an unknown phone.

What the server checks, in order

  • Sanctum: bearer token resolves to an existing user.
  • The token-holding user has is_service = true.
  • That user is attached to exactly one workspace via the workspace_members pivot with role = 'agent'. That workspace is the “agent workspace”.
  • The acting-user phone resolves to a real user (or, if missing, is auto-created with is_service=false).
  • The actor is a member of the agent’s workspace.
  • Any board_id / card_id / list_id in the request belongs to that workspace.
  • The actor’s role grants the specific permission the tool requires — one verb per operation. Examples: card.create, card.archive, card.complete, card.assign, card.label, card.move, list.create, list.unarchive, label.create, label.delete, custom_field.create, card.custom_field.set, attachment.create, attachment.delete, dependency.add, reminder.cancel, comment.create, comment.edit (own), comment.delete (own), agent.webhook.subscribe, digest.set. The full role × permission matrix lives in database/seeders/RolesAndPermissionsSeeder.php.

If any check fails, the tool returns a JSON-RPC isError: true with a human-readable message and an audit row is written with result = 'denied' and error_code = 'forbidden'.

Roles and the permission matrix

Workspace members are assigned one of five tiered roles via the workspace_members.role pivot. Each role grants a different slice of the verb catalog:

  • owner — everything, including workspace.delete and workspace.billing.
  • admin — full board/card/list/label/custom-field/webhook management; cannot delete the workspace or touch billing.
  • member — day-to-day operational work (create/edit cards, manage checklists/attachments/dependencies, watch, comment). Cannot manage labels, custom-field schemas, member roles, or agent webhooks; cannot bulk-edit, hard-delete cards, or share boards publicly.
  • guest — comment only, plus edit/delete their own comments.
  • agent — mirrors admin for operational verbs but is further constrained by the per-board agent allowlist (the scope_ok context flag set by BoardTool once the board is confirmed in-workspace). Agents cannot invite, change member roles, delete the workspace, or touch billing.

Tools

All tools are listed here grouped by category. Names use the kebab-case convention Laravel/MCP derives from the class name (CreateCardToolcreate-card-tool).

Sanity bearer token only

ToolArgumentsPermission
whoami-tool (none) bearer only

Returns agent, workspace, and server_time using just the Sanctum token. actor is included only if X-Acting-User-Phone is sent; otherwise it’s null. Use this at boot to verify the token resolves to the correct workspace before issuing any acting-user calls.

Read bearer token only

Read tools are scoped to the agent’s workspace and accept the bearer token alone — X-Acting-User-Phone is optional. If you send it, the audit row records the actor; otherwise the agent itself is recorded.

ToolArgumentsPermission
list-boards-tool (none) bearer only
get-board-tool board_id bearer only
list-cards-tool board_id, list_id?, assigned_to_user_id?, include_archived?, limit? bearer only
get-card-tool card_id bearer only
search-cards-tool board_id, query, limit? bearer only
list-card-reminders-tool card_id, status? bearer only

Board mutations requires board.create / board.update / board.archive

ToolArgumentsPermission
create-board-tool name, external_ref?, color?, visibility?, initial_lists? board.create
update-board-tool board_id + any of name, color, visibility, external_ref board.update
(or board.external_ref when external_ref is in the patch)
archive-board-tool board_id, restore? board.archive
(board.unarchive when restore=true)
find-or-create-board-by-external-ref-tool external_ref, name, color?, visibility?, initial_lists? board.create
(only on create)

Card mutations requires specific card.* / comment.* verb

ToolArgumentsPermission
create-card-tool board_id, list_id, title, description?, due_at?, due_reminder_minutes?, assignee_user_ids?, assignee_phones?, label_ids? card.create
update-card-tool card_id + any of title, description, due_at, due_reminder_minutes, cover_color, cover_label card.edit
move-card-tool card_id, target_list_id, target_board_id?, position? card.move
assign-member-tool card_id, (user_id | user_phone), action = add | remove card.assign
archive-card-tool card_id card.archive
restore-card-tool card_id card.unarchive
comment-on-card-tool card_id, body comment.create
complete-card-tool card_id, completed? card.complete

Checklists requires checklist.* / checklist_item.*

ToolArgumentsPermission
create-checklist-tool card_id, title checklist.create
add-checklist-item-tool checklist_id, body checklist_item.create
complete-checklist-item-tool item_id, completed checklist_item.update

Reminders requires reminder.cancel

ToolArgumentsPermission
cancel-card-reminder-tool dispatch_id?, card_id?, type? reminder.cancel

List structural requires list.* permission

ToolArgumentsPermission
create-list-tool board_id, name, position? list.create
rename-list-tool list_id, name list.edit
archive-list-tool list_id, restore? list.archive
reorder-lists-tool board_id, ordered_list_ids[] list.reorder

Labels requires label.create / .update / .delete

ToolArgumentsPermission
list-labels-tool board_id bearer only
create-label-tool board_id, name, color label.create
update-label-tool label_id, name?, color? label.update
delete-label-tool label_id label.delete

Attachments requires attachment.delete

ToolArgumentsPermission
list-card-attachments-tool card_id bearer only
delete-attachment-tool attachment_id attachment.delete

Card dependencies requires dependency.add / .remove

ToolArgumentsPermission
add-card-dependency-tool blocker_card_id, blocked_card_id dependency.add
remove-card-dependency-tool dependency_id? | (blocker_card_id, blocked_card_id) dependency.remove
list-card-dependencies-tool card_id bearer only

Custom fields requires custom_field.* / card.custom_field.set

ToolArgumentsPermission
list-board-custom-fields-tool board_id bearer only
create-board-custom-field-tool board_id, name, type, options? custom_field.create
delete-board-custom-field-tool field_id custom_field.delete
set-card-custom-field-tool card_id, field_id, value? card.custom_field.set

Agent webhooks requires agent.webhook.*

ToolArgumentsPermission
subscribe-agent-event-tool event, target_url agent.webhook.subscribe
unsubscribe-agent-event-tool subscription_id? | (event, target_url) agent.webhook.unsubscribe
list-agent-event-subscriptions-tool (none) agent.webhook.list

Personal digest requires digest.set

ToolArgumentsPermission
set-digest-schedule-tool frequency, hour_of_day?, day_of_week?, timezone?, active? digest.set

Smart ops utility / card.bulk_edit

ToolArgumentsPermission
parse-due-date-tool phrase, timezone? bearer only
bulk-update-cards-tool filter, patch card.bulk_edit

Analytics read-only

ToolArgumentsPermission
get-board-analytics-tool board_id, window_days? bearer only

Workspace membership requires workspace.invite

ToolArgumentsPermission
invite-workspace-member-tool phone, name?, role? workspace.invite

Worked example: create_card

JSON-RPC request
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "create-card-tool",
    "arguments": {
      "board_id": 5,
      "list_id": 10,
      "title": "Reschedule pricing review",
      "due_at": "2026-05-15T10:00:00Z",
      "assignee_phones": ["+62812..."]
    }
  },
  "id": 42
}
JSON-RPC response
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"card_id\":11,\"title\":\"Reschedule pricing review\",\"list_id\":10,\"position\":\"459769.0000000000\",\"source\":\"agent\",\"created_by_user_id\":42,\"created_via_agent_user_id\":4}"
    }],
    "isError": false
  }
}

Try it

Call any tool live against this server. Provide a workspace JAVIS bearer token and the phone of the human you’re acting on behalf of, fill in the parameters, and send. Each request is real — mutating tools will actually create or change records and write an audit row.

This is the live MCP server

Requests post to POST https://board.ssf.studio/mcp/board. Use a non-production workspace token while exploring; cards, lists, comments, and invitations created here are real. The bearer token and phone you enter are kept only in your browser’s localStorage and never leave this device unless you press Send request.

From Admin → Manage Workspaces → Provision JAVIS. Shown once when issued.
E.164 phone of a member of the JAVIS workspace.

Saved in this browser only.

Resources

Resources are read-only data the agent can fetch by URI. Useful for seeding conversation context cheaply, without invoking a tool.

URI templateReturns
board://{board_id}/snapshot Full board state: lists with their non-archived cards (id, title, due_at, source).
workspace://{workspace_id}/my-assignments Cards assigned to the acting user across all boards in the workspace, ordered by due date.
workspace://{workspace_id}/members Workspace member directory (id, name, phone, role) — useful for resolving @mentions and assignment targets.

Listing and reading

JSON-RPC · resources/templates/list
{ "jsonrpc": "2.0", "method": "resources/templates/list", "params": {}, "id": 1 }
JSON-RPC · resources/read
{
  "jsonrpc": "2.0",
  "method": "resources/read",
  "params": { "uri": "board://5/snapshot" },
  "id": 2
}

Errors & audit

Error shapes

  • HTTP 401 — bearer missing/invalid. JSON body: {"message":"Unauthenticated."}
  • HTTP 404X-Acting-User-Phone sent but the phone is not registered and the bearer is not a service token. JSON body: {"message":"Acting user not registered.","error":{"code":"phone_not_linked"}}
  • JSON-RPC isError: true — every other failure: missing acting-user (no_actor), bad arguments, no permission, missing record, cross-workspace boundary. The text content carries a human-readable reason.

Audit codes

error_codeMeaning
no_actorBearer authenticated but no actor could be resolved.
forbiddenActor is not a workspace member, board is in another workspace, or actor lacks the required permission.
not_foundBoard / list / card / user referenced does not exist.
validationArgument schema validation failed.
<ExceptionClass>Uncaught exception class basename (truncated at 64 chars). Look in Laravel logs.

Audit retention

Rows older than 180 days are pruned daily at 03:30 by php artisan agent:prune-audit. Override via --days=N or test with --dry-run.

Client integration

Any MCP-compatible client works. Below: the official TypeScript SDK pattern, since most agent stacks (including the JAVIS production agent) run in Node.

TypeScript · @modelcontextprotocol/sdk
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL(`${BASE_URL}/mcp/board`),
  {
    requestInit: {
      headers: {
        "Authorization": `Bearer ${TOKEN}`,
        "X-Acting-User-Phone": phoneOfTheUserSpeakingNow,
      }
    }
  }
);

const client = new Client({ name: "javis-agent", version: "1.0.0" });
await client.connect(transport);

// One round-trip — server returns full tool list with JSON schemas.
const { tools } = await client.listTools();

const result = await client.callTool({
  name: "create-card-tool",
  arguments: { board_id: 5, list_id: 10, title: "From Javis" }
});
Pass the actor on every call

The acting-user header is per request, not per session. If your agent is in a multi-user channel (a WhatsApp group), update the header before each call to identify the human currently speaking. The bearer token never changes.

Data model side-effects

What changes in the database when JAVIS acts.

Cards created via the agent

  • cards.source = 'agent'
  • cards.created_by_user_id = the human (the actor)
  • cards.created_via_agent_user_id = the workspace JAVIS service user
  • UI surfaces this as the “via JAVIS” badge on the card.

Events fired

Tools dispatch the same events the web UI does — CardCreated, CardUpdated, CardMoved, CardArchived, CommentAdded, ListCreated, ListMoved, ListArchived, ListUpdated. Broadcast subscribers (Reverb) and notification dispatchers see no difference between web-originated and agent-originated changes, beyond the source field.

Notifications

Assignments and due-date changes schedule the same NotificationDispatch rows that the REST API path writes. The actor is recorded on the card.assignees pivot and as the actor_id in the assignment notification payload.

Ops & troubleshooting

Rotating a token

Admin → Manage Workspaces → Provision JAVIS on the affected workspace. The previous token is deleted; the new plaintext is shown once. Live agent processes need to be restarted with the new token.

Disabling JAVIS for a workspace

Delete the JAVIS user’s tokens ($javis->tokens()->delete()) — every subsequent MCP call returns 401, but no kanban data is affected. The user row and audit history remain.

Pagination on tools/list

Default page size is 10 tools. The response includes nextCursor when there are more. JAVIS’s production client should walk the cursor; ad-hoc curl callers can pass ?per_page=50 to get them all in one shot.

Local debugging

Run php artisan mcp:inspector to open the Laravel/MCP web inspector against your local server. Useful for poking at tool schemas without writing a client.

Appendix · file map

Where to look in the codebase, in execution order.

FileRole
routes/ai.phpRegisters Mcp::web('/mcp/board', BoardServer::class) with auth:sanctum + acting-user middleware.
app/Http/Middleware/ResolveActingUser.phpReads X-Acting-User-Phone, swaps the auth user to the actor, stashes the original service caller as service_caller on the request.
app/Mcp/Servers/BoardServer.phpRegisters all 16 tools and 3 resources.
app/Mcp/Tools/BoardTool.phpBase class. Wraps tool execution in workspace-boundary checks, validation, audit, and structured error mapping.
app/Mcp/Support/McpContext.phpPure helpers: actor(), agent(), agentWorkspaceId(), assertActorInAgentWorkspace(), assertBoardInWorkspace(), audit().
app/Mcp/Tools/*.phpOne file per tool. Each is a thin adapter that validates input and delegates to an App\Actions\* class.
app/Actions/Cards/*, app/Actions/Lists/*Business logic. The same actions are intended to back Livewire and (eventually) the REST controllers, so MCP and the UI never drift.
app/Mcp/Resources/*.phpURI-templated resources for read-only context.
app/Console/Commands/Mcp/PruneAgentAudit.phpDaily prune of agent_call_audit rows older than 180 days.
database/migrations/…_add_created_via_agent_user_id_to_cards_table.phpAdds the via-agent FK column on cards.
database/migrations/…_create_agent_call_audit_table.phpCreates the audit table.