heihallo/mcp-kit
A Laravel toolkit for building Model Context Protocol servers, helping you expose tools, resources, and prompts to AI clients with clean, framework-native APIs and minimal setup.
Read the source before you install. At zero stars and zero dependents you are the entire QA function for this package. Budget an hour to skim src/, config/, and database/migrations/ before it touches a real app. Nothing below should be taken as verified API surface — treat class and command names as shapes to confirm against the repo.
Minimal path:
composer require heihallo/mcp-kit (pin an exact version, not ^).php artisan vendor:publish --tag=mcp-kit-config and --tag=mcp-kit-migrations, then php artisan migrate.php artisan route:list | grep -i mcp to see what endpoint you now expose.register() call in a service provider. This file is your allowlist and the single most important thing to read.php artisan mcp-kit:token {user} or equivalent.First use case, deliberately boring: expose exactly one read-only capability — "look up an order by ID" — delegating to an existing Action or query you already trust. Issue a personal token, point an MCP client (Claude Desktop, Cursor, or mcp-inspector) at the endpoint, and confirm three things:
If the audit row is missing, stop and fix that before adding any write capability. Audit is the feature you cannot retrofit.
The second thing to wire is a write capability in preview/confirm mode, because that's the package's actual differentiator and where the integration risk lives.
Wrap, don't reimplement. Every tool should be a thin adapter over an Action/Service class the rest of your app already uses. The moment a tool contains its own validation or query logic, you have two code paths that will drift and only one of them is covered by your existing tests. Aim for tools that are ~15 lines: validate input, call the Action, shape the response.
Catalogue as opt-in allowlist. Resist any auto-discovery of "public methods" the package might offer. An explicit registry means every new agent capability is a reviewable one-line diff in a PR, which is exactly the review gate you want for anything that can mutate data.
Per-person tokens, per-device scoping. Tokens should map to a real users row, never to the application itself, and ideally to a user_id + device_label + expires_at triple. Practical flow:
revokeAllForUser() call in the same listener that deactivates the account,Authorization stays in Laravel. Route tool-level access through Gate::forUser($tokenUser)->allows(...) against policies you already maintain, rather than a parallel ACL in the package's config. The access middleware should be the enforcement point; the config should only decide which ability name a tool maps to.
Preview/confirm as a two-phase protocol. The pattern that works:
preview_refund(order_id, amount)
-> { diff: "...", confirm_token: "abc", expires_at: "..." }
apply_refund(confirm_token)
-> executes, or 409 if expired/replayed/args changed
Rules that matter: bind the confirm token to the exact argument payload (hash it in), make it single-use, give it a short TTL (2–5 minutes), and make the effect idempotent anyway. The preview response should render the human-readable consequence — "refund $40.00 to Jane Doe, order #1234" — because that string is what the person in the chat window actually approves. If the model can summarize it lossily, the confirmation is theatre.
Audit as a first-class record. Log user_id, token_id, tool_name, hash of arguments, outcome, latency, and request correlation ID. Two decisions to make up front: redact argument values that can contain PII or secrets (log the hash, not the body, for anything touching auth or payment data), and decide whether the audit write shares a transaction with the mutation. For writes, it usually should — an unlogged refund is worse than a failed one.
Ground rules as data, not prose scattered in prompts. Keep them in a versioned config file: what's off-limits, when to escalate to a human, which environments the agent should refuse to touch. Coarse policy survives model changes; per-tool usage documentation inside the rules block just burns context.
Memory: namespaced, bounded, untrusted. Key by user_id, cap entry count and total bytes, add TTL, and expose an explicit forget tool. On read, treat memory as untrusted input — it's user-influenced text re-entering the model's context, so don't let it carry instructions that override ground rules.
Onboarding as a tool. A what_can_i_do capability that returns the caller's permitted tools plus a one-line description and a docs link replaces a lot of Slack questions and gives new staff a self-serve map on day one.
Long operations: return a job ID and a check_status tool instead of holding the MCP call open. Clients time out, and a timed-out write with unknown outcome is the worst possible state.
You are the maintainer. Zero dependents means no Stack Overflow answers, no blog posts, no one else hitting the bug first. Pin the version in composer.json, keep a fork or a patch set ready, and assume you'll own fixes. Write an integration test per tool now, while the surface is small.
Tokens end up in plaintext on disk. MCP client configs are JSON files in home directories, frequently synced to cloud drives and occasionally committed. Scope them narrowly, expire them, and consider a per-device token so a single leak is a single revocation.
The confirm token gets lost between turns. Agents truncate context, clients restart, users interleave questions. Design so a lost token degrades gracefully: the agent can re-run preview, and the error message says exactly that. Don't make confirm tokens so opaque that recovery requires a human with database access.
Replay and TOCTOU. A confirm token that isn't bound to its argument hash lets a caller preview a $5 refund and confirm a $500 one. Bind it. Single-use it. And re-check authorization at confirm time, not just at preview — roles change mid-conversation more often than you'd think.
Stale permission caches. If tool lists or scopes are cached per token, a revoked role can keep working until TTL expiry. Either keep authorization checks uncached (cheap, hits Gate) or bust the cache in the same place you mutate roles.
Tool-count bloat degrades selection quality. Fifteen tools with overlapping descriptions is measurably worse than five crisp ones. Group related reads into one parameterized tool, and prune anything unused after a month — the audit log tells you exactly which tools nobody calls.
Input schemas are a public contract. Once a colleague's saved prompt or a client-side workflow depends on a parameter name, renaming it silently breaks them. Additive changes only; deprecate with a version bump.
Audit table growth and PII. Estimate volume before launch — an active user can generate hundreds of rows a day. Set a retention window at creation time rather than discovering the problem at 10M rows, and never let audit logging become the reason a SELECT on user data gets slow.
Debugging: you want the raw JSON-RPC frames when a tool "isn't being called." Add a debug log channel behind an env flag that dumps request/response bodies with token values redacted, and verify the endpoint with mcp-inspector outside your chat client — it removes the client from the variable set and shows you the exact schema the model is being given.
Local development: seed a dev token via a seeder, never commit a real one, and point the client at your herd/valet host. Confirm the package doesn't register its routes in production by default — if it does, that's your first patch.
Extension points worth checking in source: the middleware stack order (access before audit, or audit before access — you want the denial logged too), whether the audit middleware sees the tool name before or after resolution, and whether memory/onboarding are swappable via container bindings. If they're hardcoded, prefer composing around them rather than forking.
How can I help you explore Laravel packages today?