Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Relay Laravel Package

prism-php/relay

Relay is an MCP client for Prism that lets Laravel and PHP applications connect AI agents to external Model Context Protocol servers, discover their tools, resources, and prompts, and call them through a clean, fluent API.

Deep Wiki
Context7

Getting Started

Relay is the bridge between a Prism agent and one or more MCP servers. Prism already knows how to call tools; Relay's job is discovery and transport — it dials the MCP server, performs the initialize handshake, lists the tools, and hands back objects that satisfy Prism's tool contract. Nothing else in your Prism code changes.

Install and publish config:

composer require prism-php/relay
php artisan vendor:publish --tag="relay-config"

Where to look first, in this order:

  1. config/relay.php — the servers array. Every entry is one MCP endpoint (stdio command or HTTP/SSE URL).
  2. The package's src/Tools/ directory — this is where the MCP inputSchema gets translated into a Prism-compatible tool. Read this before debugging any "the model calls the tool with wrong arguments" complaint.
  3. The Relay facade — the entry point you'll actually call from application code.

Minimal config:

// config/relay.php
return [
    'servers' => [
        'filesystem' => [
            'transport' => 'stdio',
            'command' => ['npx', '-y', '@modelcontextprotocol/server-filesystem', base_path()],
            'timeout' => 30,
        ],
        'docs' => [
            'transport' => 'http',
            'url' => env('MCP_DOCS_URL'),
            'headers' => ['Authorization' => 'Bearer '.env('MCP_DOCS_TOKEN')],
        ],
    ],
];

First use case — the five-minute win:

use Prism\Prism\Facades\Prism;
use Prism\Relay\Facades\Relay;

$response = Prism::text()
    ->using('anthropic', 'claude-sonnet-4-5')
    ->withPrompt('Read README.md and summarise the installation steps.')
    ->withTools(Relay::server('filesystem')->tools())
    ->withMaxSteps(5)
    ->asText();

Verify the wiring before you involve a model:

php artisan relay:list filesystem      # confirm the server starts and enumerates tools
php artisan relay:call filesystem read_file --args='{"path":"README.md"}'

If relay:list is empty, you have a transport problem, not a prompt problem. That distinction saves hours.

Implementation Patterns

Scope tools per server, not globally. Relay is happy to return every tool from every configured server, but dumping 40 tools into a prompt costs tokens and measurably degrades tool selection. Select deliberately:

$tools = Relay::server('filesystem')->only(['read_file', 'list_directory'])->tools();
// or compose across servers, which is where this gets genuinely useful
$tools = collect()
    ->merge(Relay::server('filesystem')->tools())
    ->merge(Relay::server('github')->only(['search_issues'])->tools())
    ->all();

Treat MCP as a capability layer for existing Prism agents. The highest-leverage pattern is bolting Relay onto an agent you already have. If your agent currently has three hand-written Tool classes, add an MCP server's tools alongside them — Prism does not care where a tool came from, so mixing is free and lets you migrate incrementally rather than rewriting.

Cache the tool manifest. tools() triggers a process spawn or HTTP round-trip plus a protocol handshake. Doing that on every web request is the single most common performance mistake. Cache the manifest and construct fresh tool objects from it:

$manifest = Cache::remember("mcp:tools:{$server}", now()->addMinutes(10), fn () =>
    Relay::server($server)->tools()
);

Invalidate on deploy or when the MCP server version changes.

Push slow tool execution to the queue. MCP tool calls are synchronous from Prism's perspective, but nothing stops the tool implementation underneath from dispatching a job. For anything hitting an external system — issue trackers, internal APIs, databases — wrap the call and give the model a job-ID handle to poll. This keeps the Prism step loop from blowing your request timeout.

Use resources as pre-flight context, not as tools. MCP servers publish resources (documents, schemas, config) as well as tools. Rather than making the model call read_resource every time, fetch the relevant ones up front and inject them into the system prompt. Fewer round trips, better cache behaviour on the provider side.

Multi-server agents need name collision handling. Two servers both exposing search will produce two identically-named Prism tools and undefined behaviour. Prefix on the way out:

Relay::server($name)->tools()->map(fn ($tool) => $tool->withName("{$name}__{$tool->name()}"));

Testing. Use Prism::fake() for the model side and Relay::fake() (or an in-memory MCP server over stdio) for the transport side. Assert on the tool calls the model would have made rather than actually spawning npx in CI — process spawns in a test suite are slow and flaky on macOS runners.

Gotchas and Tips

stdio transport means process spawning — check your runtime. Serverless platforms, many container runtimes, and shared hosts either cannot fork npx/uvx at all or kill long-lived children between requests. If you're on Lambda or Vercel, use the HTTP/SSE transport or don't use Relay. This is the first thing to verify and the most common reason a working local setup fails in production.

Long-lived clients and Octane don't mix well. An MCP session has state (initialization, negotiated protocol version, session ID). A singleton resolved at boot will be reused across requests under Octane and will eventually serve a stale session. Bind Relay's client with scoped() rather than singleton(), or reconnect per request.

Tool names must survive the provider's constraints. OpenAI-compatible endpoints enforce ^[a-zA-Z0-9_-]{1,64}$ on function names. MCP servers routinely publish names with dots, colons, or slashes (fs.read_file, github/issues:search). Relay may sanitise these for you — confirm in src/Tools/ — and if it doesn't, sanitise yourself, but keep a reverse map so the response handler routes to the original MCP name.

JSON Schema translation is lossy. MCP tools use full JSON Schema; Prism tools generally express parameters as an array of typed properties. Constructs like oneOf, anyOf, nested objects, and $ref may be flattened or dropped. The symptom is subtle: the model calls the tool correctly but with a flat argument where the server expects nesting. Test any tool with a non-trivial schema manually via relay:call before trusting the model with it.

Return errors as tool results, not exceptions. If an MCP call throws, Prism aborts the whole step loop. Catch transport and server errors and return a structured failure ("error": "file not found") as the tool output instead — the model will usually self-correct and pick another tool, and you get one extra step rather than a 500.

Timeouts are layered and confusing. There's your HTTP request timeout, Prism's own step timeout, Relay's timeout config, and the MCP server's internal timeout. When something hangs at 30s, it's almost always Relay's config default, not Prism. Set timeout explicitly per server and log the start/finish of every tool call during development.

Log the raw MCP traffic behind a flag. Add a RELAY_DEBUG=true config switch that dumps JSON-RPC frames to a dedicated log channel. When the model insists a tool doesn't exist or the schema comes back mangled, the wire log answers it in seconds; the Prism-side abstraction will not.

Watch token cost as servers grow. Every tool's name, description, and schema is billed on every request in the step loop. A verbose MCP server with 20 tools can add 3–5k tokens of overhead per call. If you notice cost creep after adding a server, that's why — prune with only(), or split into two agents with narrower tool sets.

Extension point worth knowing: if the bundled transport doesn't fit (WebSocket, a custom socket, an in-process PHP MCP server), the client/transport contract is the seam to implement. Write a small class satisfying it, register it in config under transports, and everything above Relay's transport layer — schema mapping, tool wrapping, the facade — keeps working unchanged.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo