boneframework / mcp
MCP for Bone Framework
Requires
- php: ^8.2
- ext-intl: *
- boneframework/bone: ^3.9
- boneframework/contracts: ^v1.0
- mcp/sdk: ^0.9.0
Requires (Dev)
- delboy1978uk/dev-tools: ^1.0.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Installation
Install via composer:
composer require boneframework/mcp
Configuration
1. Register the Package
Enable the package in your config/packages.php:
<?php return [ 'packages' => [ Bone\BoneDoctrine\BoneDoctrinePackage::class, App\AppPackage::class, Bone\MCP\MCPPackage::class, ], ];
Register it after the packages that provide any services you use as middleware (for example Bone\OAuth2\BoneOAuth2Package).
2. Configure the MCP server
Create config/mcp.php. Every key is optional; the defaults are shown:
<?php return [ 'mcp' => [ 'path' => '/mcp', 'server_name' => 'Bone Framework MCP Server', 'server_version' => '1.0.0', 'instructions' => null, // sent to clients on initialize: tell agents how to use your tools // where your tool classes live; relative paths are resolved from the application root 'discovery' => [ 'base_path' => 'src', 'scan_dirs' => ['Mcp'], 'exclude_dirs' => [], ], // sessions must outlive a single PHP request, so they are stored on disk 'session' => [ 'directory' => 'data/cache/mcp-sessions', 'ttl' => 3600, 'lock_timeout' => 30, // seconds a request waits for another request on its session (then 503) ], // Host headers accepted by the DNS rebinding protection (no ports) 'allowed_hosts' => ['localhost', '127.0.0.1', '[::1]'], // cross-origin callers (browsers send Origin), e.g. 'https://app.example.com'; same-origin is always allowed 'allowed_origins' => [], // PSR-15 middleware applied to the endpoint: container ids/class names or instances 'middleware' => [], ], ];
3. Secure the endpoint
The endpoint is open unless you add middleware. With delboy1978uk/bone-oauth2 installed,
require a Bearer token like this:
'middleware' => [Bone\OAuth2\Http\Middleware\ResourceServerMiddleware::class],
If you serve the app on a hostname other than localhost, add it to allowed_hosts, e.g.
'allowed_hosts' => ['localhost', $_ENV['DOMAIN_NAME']], or every request gets
403 Forbidden: Invalid Host header.
Server-side clients send no Origin header. Browser-based clients on another origin must be
listed in allowed_origins (scheme, host and port), or they get 403.
Writing tools
Put tool classes in src/Mcp/ (or wherever discovery points) and mark public methods with
#[McpTool]. The input schema is generated from the parameter types and docblock.
<?php namespace App\Mcp; use Bone\MCP\RequestContext; use Mcp\Capability\Attribute\McpTool; use Mcp\Exception\ToolCallException; class GreetingTools { public function __construct(private RequestContext $context) {} /** * Greet someone. * * @param string $name who to greet */ #[McpTool(name: 'greet')] public function greet(string $name): array { if ($name === '') { throw new ToolCallException('name is required'); // returned to the agent as a tool error } return ['greeting' => 'Hello ' . $name, 'client' => $this->context->getClientId()]; } }
Tool classes are resolved from the Bone container, so register any class with constructor
dependencies in your package's addToContainer():
$c[GreetingTools::class] = $c->factory(fn (Container $c) => new GreetingTools($c->get(RequestContext::class)));
Bone\MCP\RequestContext holds the HTTP request being served: use getAttribute() for any request
attribute, or getClientId() for the oauth_client_id set by the OAuth resource server middleware.
Throw Mcp\Exception\ToolCallException for errors the agent should see; any other exception is
reported as an opaque internal error.
Usage
The endpoint speaks MCP's Streamable HTTP transport:
- POST
/mcp— JSON-RPC messages.initializereturns anMcp-Session-Idheader; send it on every later request - GET
/mcp— server-sent event stream for the session - DELETE
/mcp— end the session
POST /mcp Content-Type: application/json Accept: application/json, text/event-stream Authorization: Bearer <token> {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "MyClient", "version": "1.0.0"}}}
Note that clientInfo.version must be a non-empty string other than "0".
Concurrent requests
Clients may send several requests on one session at once (e.g. parallel tool calls). The package
configures the SDK's per-session lock (Mcp\Server\Session\FileSessionLock, flock on
<session id>.lock next to the session files): requests on the same session run one at a time
and each gets its own response, while different sessions still run in parallel. A request that
waits longer than session.lock_timeout is answered with 503. The session directory must be on
a filesystem that supports flock (local disks and Docker volumes do; some network filesystems
don't). The session store's garbage collection removes lock files of expired sessions.
Development
Running Tests
vendor/bin/codecept run Unit --coverage
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Credits
- Derek McLean - @delboy1978uk
License
MIT License. See the LICENSE file for details.