vinkius/laravel-mcp-connect
A Laravel package for connecting your application to Model Context Protocol (MCP) servers and tools. It streamlines MCP integration so Laravel apps can expose resources, prompts, and tools to AI clients with familiar configuration and an elegant developer experience.
Install and publish config
composer require vinkius/laravel-mcp-connect
php artisan vendor:publish --tag=mcp-connect-config
Add your Vinkius token to .env (the package ships a config entry for it — check config/mcp-connect.php for the exact key, typically MCP_CONNECT_TOKEN). The token is only needed for the Catalog API; the Marketplace API is public and works without one, which makes it safe to wire into local/dev environments immediately.
Where to look first
config/mcp-connect.php — base URLs, token, cache TTLs, retry settings, event toggles. Read this before writing any code; most "why is it doing that?" questions are answered here.Mcp facade — the primary entry point (Mcp::tools(), Mcp::execute(), Mcp::listings(), Mcp::categories(), etc.).src/DTOs or similar) — 13 readonly classes; these tell you exactly what shape every response has.Your first use case: discover → inspect → execute
Do this from the CLI before writing a line of application code. It's the fastest way to build a mental model of the payload shapes:
php artisan mcp:browse # public marketplace, no token required
php artisan mcp:search "pdf" # find servers/tools
php artisan mcp:inspect <id> # see the tool schema and required args
php artisan mcp:execute <tool> --arg=value
Then mirror the same flow in code:
use Vinkius\McpConnect\Facades\Mcp;
$tools = Mcp::tools($serverId); // returns readonly DTOs
$result = Mcp::execute($toolId, [
'query' => 'quarterly report',
]);
If mcp:browse works but mcp:tools fails, your problem is auth, not the package.
Keep the dual API surface explicit. Treat the Marketplace API as your "discovery / read-only" layer (safe to call in dashboards, admin UIs, seeders, local dev) and the Catalog API as your "authenticated / action" layer. Wrapping each in its own small service class (MarketplaceBrowser, CatalogExecutor) prevents the Mcp facade from spreading across your codebase and makes it obvious at the call site which credential path is in play.
Wrap execution in an application-level action. Tool execution is an outbound network call with side effects — don't call it inline from controllers. Put it behind a queued job or a dedicated action class so you get retries, timeouts, and audit logging on your own terms rather than relying solely on the package's HTTP retry:
final class ExecuteMcpTool
{
public function __construct(private readonly string $toolId) {}
public function handle(array $arguments): ToolResult
{
return Mcp::execute($this->toolId, $arguments);
}
}
Lean on the events for observability. ToolExecuted, ServerActivated, and friends are the intended integration seam. Register listeners in EventServiceProvider (or use Event::listen) to forward to your logging/analytics pipeline. Because events are dispatched in-process, keep listeners fast — push anything slow (Slack notifications, warehouse writes) onto a queue.
Use DTOs as your boundary, then map. The 13 readonly DTOs are typed and have helper methods, but don't leak them into your domain models or Blade views. Map to your own value objects or plain arrays at the edge of your integration. The readonly nature is a feature here — it guarantees a response can't be mutated mid-request by an over-eager listener.
Exploit the caching layer rather than building your own. The read-through cache with per-endpoint TTLs is exactly what you'd otherwise hand-roll around listing/category calls. Tune TTLs per endpoint in config: listings and categories change slowly (hours), tool schemas moderately (minutes), execution results never (execution should bypass cache — verify this in config before assuming).
Artisan commands are first-class workflow tools, not just debugging aids. Reasonable day-to-day uses:
mcp:search / mcp:browse in onboarding docs and internal wikis.mcp:inspect in CI to assert a tool's schema hasn't drifted between deployments.mcp:analytics as a scheduled command writing usage snapshots to your DB.mcp:execute in one-off ops runbooks (php artisan mcp:execute ...) — never in a request path.Test with Http::fake() and Event::fake(). With 59 Pest tests shipped, the package's own suite is a good template. In your app, fake the HTTP layer to assert you're sending the right arguments, and Event::fake() to assert ToolExecuted fires with the expected DTO — that keeps tests fast and offline.
Execution retries are not automatically idempotent. The HTTP transport ships with retry support. For read-only catalog calls that's free. For execute, a retried request can mean a tool ran twice. Check what the retry policy covers (connection failures vs. 5xx vs. all) and consider disabling retries for execution in config, handling retries yourself with a dedupe/idempotency key if the target tool supports one.
Cache invalidation after activation. ServerActivated changes which tools are available. If your tool-list cache TTL is generous, you'll be staring at a stale list wondering why the new server isn't usable. Either shorten that TTL or explicitly bust the cache in your ServerActivated listener — this is the single most likely "it's not working" moment for a new integrator.
Config is cached in production. php artisan config:cache will freeze your token and TTLs. After changing config/mcp-connect.php, run php artisan config:clear locally and redeploy (not just restart) in production.
Readonly DTOs won't serialize the way you expect. Don't json_encode a DTO and assume it matches the raw API payload — use the provided toArray()/helper methods. If you need the untouched response, capture it before mapping.
Distinguish error classes, not status codes. The package's structured error classification exists so you don't have to parse HTTP codes. Catch the specific exception types (auth vs. not-found vs. rate-limit vs. validation) and handle them differently — a 429 wants backoff, a 404 means the server/tool ID is wrong and retrying is pointless. Avoid a blanket catch (\Throwable).
Rate limits are real on 3,400+ servers. Bulk-discovery loops (foreach over every category calling mcp:tools) will hit limits quickly. Prefer one listing call plus cached lookups, and lean on the per-endpoint cache instead of fanning out.
Version and runtime constraints are tight. PHP 8.2+, Laravel 11 or 12 only. If you're still on Laravel 10, this package is a non-starter — plan the framework upgrade first. The readonly DTOs and typed mapping also mean 8.1 is genuinely unsupported, not just formally so.
Extension points to know. The publishable config is the primary extension surface (base URLs, timeouts, TTLs, retry, token). Beyond that, the event system is the sanctioned way to hook in. If you need to reshape the HTTP layer itself, check whether the transport is bound in the container before reaching for a decorator — swapping a binding is cleaner than wrapping the facade.
Production safety on destructive commands. mcp:execute can trigger real side effects. Gate it behind an environment check in your own tooling, and be careful about putting it in any automated pipeline that runs against production credentials.
How can I help you explore Laravel packages today?