Skip to content
LaravelBlog

Add config-driven MCP clients with GitHub, Linear and Playwright presets

LaravelBlogBot
LaravelBlogBot

Contributed by pushpak1300

Part of the mcp v1.0.2 release

PR #357 turns MCP client wiring into configuration. On v1.0.1, every connection to an external MCP server required a Mcp::registerClient() closure and whatever ad-hoc config keys you invented yourself. This release adds a clients section to config/mcp.php so a named client is a single array entry away, and it ships ready-made presets for GitHub, Linear, and Playwright. It also introduces wildcard-based tool allow/block lists and adds environment variable support for local stdio clients. Existing registerClient() closures keep working and still take priority, so the upgrade path is deliberately conservative.

Clients now live in config/mcp.php

ClientManager::build() now resolves a client in three steps: a registered closure first, then an array under mcp.clients.<name>, otherwise a ClientException is thrown. Web-style entries use url, token, headers, oauth, and timeout; local entries use command, args, and env. Both shapes accept cache (an array with store/for, or true for the defaults), protocol_version, and tools.only / tools.except. Empty values in headers and env are filtered out, which plays nicely with env() defaults.

'clients' => [
    'sentry' => [
        'url' => 'https://mcp.sentry.dev/mcp',
        'oauth' => [
            'client_id' => env('SENTRY_MCP_CLIENT_ID'),
            'client_secret' => env('SENTRY_MCP_CLIENT_SECRET'),
        ],
        'cache' => ['store' => 'redis', 'for' => 'tenant-a'],
        'tools' => ['except' => ['delete_*']],
    ],
    'weather' => [
        'command' => 'php',
        'args' => ['artisan', 'mcp:start', 'weather'],
        'env' => ['WEATHER_API_KEY' => env('WEATHER_API_KEY')],
    ],
],

Because config clients resolve through the same Mcp::client('name') API, they drop straight into existing server code:

public function tools(): iterable
{
    return [
        ...Mcp::client('github')->tools(),
        ...Mcp::client('sentry')->tools(),
    ];
}

Tool allowlists and blocklists

The Client class gains onlyTools(), exceptTools(), and allowsTool(), all matching with Str::is() so patterns like *_issue or delete_* work. The enforcement is two-sided: a tool excluded by the filters is filtered out of tools() listings, and calling it anyway throws a ClientException ("MCP tool [x] is not allowed on this client"). The filters also survive serialization, so clients that get cached or session-persisted keep their guardrails after being restored — true for both named and unnamed clients. This makes it safe to hand a broad public MCP server to an agent while keeping destructive tools out of reach.

The presets: read-only GitHub by default

Three presets ship in the default config. github points at https://api.githubcopilot.com/mcp/, reads its token from MCP_GITHUB_TOKEN, and sets the X-MCP-Readonly: true header — so by default an agent cannot write to your repositories. To allow writes, you must explicitly opt in:

'github' => [
    'url' => env('MCP_GITHUB_URL', 'https://api.githubcopilot.com/mcp/'),
    'token' => env('MCP_GITHUB_TOKEN'),
    'headers' => [], // removes the read-only header
],

linear is a simple token-authenticated web client, and playwright runs headless via npx -y @playwright/mcp@latest --headless, giving you browser automation with zero setup.

Local clients and smaller internals

Client::local() accepts a third $env argument, which StdioTransport forwards to the underlying Process. The transport recipe now includes env, and TransportFactory validates it when rebuilding a serialized client. Two smaller changes ride along: token callbacks may now resolve to null (a docblock widening on HttpTransport and WebClient), and several ternaries in Server, Primitive, and Resource were refactored to null coalescing with no behavior change.

Upgrading from v1.0.1

Republish the config (php artisan vendor:publish --tag=mcp-config --force) or copy the new clients block into your existing config/mcp.php. Nothing else is required:

  • Config entries are only consulted when no closure with the same name is registered, so current registerClient() calls behave identically.
  • Presets are inert until you call Mcp::client('github'), Mcp::client('linear'), or Mcp::client('playwright').
  • Resolution happens only through Mcp::client('name') — there are no dynamic Mcp::github()-style facade methods to migrate to.
  • New failure modes are helpful ones: an entry with neither url nor command throws a ClientException telling you exactly which key is missing.

Takeaways

  • Define MCP clients declaratively in config/mcp.php; registered closures still take precedence.
  • GitHub, Linear, and Playwright presets are built in — GitHub is read-only until you remove the X-MCP-Readonly header.
  • Use tools.only / tools.except (wildcards supported) to hide and block tools per client; excluded tools throw on call.
  • Local stdio clients now accept env, and tool filters persist through serialization.
  • Upgrade impact is minimal: republish the config, then reach for Mcp::client() as needed.

Sources

More from this release

Related Articles