SDKs

PHP SDK

The planned PHP SDK (spacefix/sdk): what it will look like, where it can run, and what to use until it ships.

This page previews the planned API so you can see how a PHP agent will feel. Nothing below can be installed yet.

Where it can run

An agent holds one HTTP connection open for as long as it lives, often days. PHP's classic deployment (PHP-FPM or mod_php, one process per request) is built to do the opposite, so the PHP SDK needs a long-running worker process, not a request-per-process web deployment.

The agent needs a process that stays alive between calls, can read the connection and run handler code at the same time, and receives SIGTERM and gets to finish. It cannot run inside a web request: a controller that connected would hold a worker forever, die at the time limit, and register tools that vanish the moment it exits. The SDK is designed to refuse to connect under a web SAPI (php-fpm, apache2handler, cgi-fcgi) and throw UnsupportedEnvironmentException.

ShapeWhat it is
CLI daemon (recommended)php bin/svc-agent, run by systemd, Supervisor, Docker or Kubernetes, as its own process type next to your web tier
Swoole or OpenSwoole workerThe same daemon started with the Swoole transport; good if your app already runs on Swoole
RoadRunner serviceRoadRunner runs the daemon under its service plugin. The agent is not a RoadRunner HTTP worker
Laravel Octane, alongsideOctane serves HTTP; a separate php artisan svc:agent process runs next to it. Do not start the agent inside an Octane worker
Not supportedPHP-FPM, mod_php, CGI, serverless (Lambda in web mode, Vercel PHP), shared hosting, short-lived cron jobs

Until the SDK ships, the same constraint applies to anything you build yourself against the agent protocol.

What it will be

ItemPlanned value
Composer packagespacefix/sdk (namespace Spacefix\Svc)
PHP8.3 and newer
Extensionsjson, mbstring. Optional: pcntl (signals), swoole or openswoole, ev or uv
EnginesAmp 3 (default, fibers), ReactPHP, or Swoole coroutines
Companion packagesspacefix/sdk-laravel, spacefix/sdk-symfony, spacefix/sdk-testing
Core dependenciespsr/log, plus the chosen engine's HTTP client

Like every SDK, it connects out to the gateway (no inbound port), registers your tools, resources and prompts, runs your handlers when calls arrive, reconnects with backoff, and can report telemetry. See the SDK overview.

Install

bash
composer require spacefix/sdk
composer require amphp/http-client          # default engine; or react/http, or ext-swoole
composer require spacefix/sdk-laravel   # optional

A minimal agent

bin/svc-agent
<?php
declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Spacefix\Svc\Agent;
use Spacefix\Svc\Attributes\{Tool, Param};

final class OrderTools
{
    public function __construct(private OrderRepository $orders) {}

    #[Tool(description: 'Look up a single order by its id. Returns null if it does not exist.')]
    public function getOrder(#[Param('Order id, for example o_123')] string $id): ?array
    {
        return $this->orders->find($id);
    }
}

$agent = Agent::create(
    apiKey: getenv('SPACEFIX_API_KEY') ?: '',        // the URL comes from SPACEFIX_SERVICE_URL
);

$agent->register(new OrderTools(new OrderRepository(/* ... */)));

// Connects, reconnects, and returns after SIGINT or SIGTERM.
exit($agent->run());
bash
export SPACEFIX_API_KEY=agent_key_from_the_dashboard
export SPACEFIX_SERVICE_URL=https://api.spacefix.io      # optional; defaults to the production URL
php bin/svc-agent
# [svc-sdk] connected with 1 tool(s)

run() is the blocking entry point for the CLI daemon: it starts the event loop, installs SIGINT and SIGTERM handlers (this needs pcntl), connects, and returns an exit code when the agent stops (0 after a clean shutdown).

Defining a tool

PHP 8 attributes describe tools; parameter types, readonly classes and backed enums feed a schema generator that reads them once at registration.

php
use Spacefix\Svc\Attributes\{Tool, Param};

enum Status: string { case Open = 'open'; case Shipped = 'shipped'; case Cancelled = 'cancelled'; }

final readonly class SearchArgs
{
    public function __construct(
        #[Param('Customer id, for example cus_123')] public string $customerId,
        #[Param('Order status')] public Status $status = Status::Open,
        #[Param('Maximum orders to return', min: 1, max: 100)] public int $limit = 20,
    ) {}
}

final class OrderTools
{
    public function __construct(private OrderRepository $orders) {}

    #[Tool(name: 'search_orders',
           description: "Search a customer's orders by status. Returns at most `limit` orders, newest first.")]
    public function search(SearchArgs $args): array
    {
        return $this->orders->search($args->customerId, $args->status, $args->limit);
    }
}

A tool method takes either one class parameter (a readonly class whose constructor parameters are the properties) or several scalar and enum parameters. Names default to the method name in snake_case and must match ^[a-zA-Z0-9_-]{1,64}$. Resources and prompts use #[Resource] and #[Prompt]. Closures and raw JSON Schema are also supported:

php
$agent->tool(
    name: 'get_order',
    description: 'Look up a single order by id.',
    handler: fn (string $id) => $orders->find($id),
    params: ['id' => new Param('Order id')],
);

Configuration

php
$agent = Agent::create(
    apiKey: '...',                         // required
    url: 'https://api.spacefix.io',        // optional
    otel: Otel::options(argKeys: true),    // optional; otel: false disables
);
OptionDefaultMeaning
apiKeynone, requiredThe agent key. An empty value throws \InvalidArgumentException('apiKey is required.')
urlSPACEFIX_SERVICE_URL, then https://api.spacefix.ioThe gateway base URL. Set it per environment
otelon when open-telemetry/api is installedfalse disables the bridge. argKeys: true also records argument names, never values

The design also proposes transport, logger, maxConcurrentCalls, maxResultBytes, shutdownGrace, onStateChange and callScope.

Running in production

Run the daemon under a supervisor, as its own process type:

; /etc/supervisor/conf.d/svc-agent.conf
[program:svc-agent]
command=php /var/www/app/bin/svc-agent
numprocs=2                       ; two processes, two connections (check your plan's connection limit)
process_name=%(program_name)s_%(process_num)02d
autorestart=true                 ; the SDK exits 75 to be recycled (maxCalls, maxMemory)
stopsignal=TERM
stopwaitsecs=30                  ; above shutdownGrace
environment=SPACEFIX_API_KEY="%(ENV_SF_API_KEY)s"
FROM php:8.3-cli
RUN docker-php-ext-install pcntl
COPY . /var/www/app
CMD ["php", "/var/www/app/bin/svc-agent"]   # run with `docker run --init` so SIGTERM reaches PHP
  • Concurrency. Each call runs in its own fiber (or coroutine). With ordinary blocking database drivers, one process serves one call at a time, so scale with more processes (numprocs). Each process is its own agent with its own connection, and the gateway spreads calls across them, so keep state in a database.
  • State leakage. A daemon is a long-lived PHP process, unlike the per-request model most PHP code assumes. Static properties, singletons, open database connections and ORM identity maps live for days. A CallScope hook runs before and after each call so you can reset them, and the Laravel and Symfony packages wire the framework's own reset mechanisms into it.
  • Memory. maxCalls and maxMemory make the daemon exit with code 75 so the supervisor recycles it before PHP runs out of memory.
  • Shutdown. disconnect() is idempotent; run() waits up to shutdownGrace for running handlers. Under Docker the daemon must be PID 1 or run under an init (docker run --init) so SIGTERM arrives.
  • Health. $agent->state() returns State::Connecting, Connected, Retrying or Stopped. A daemon has no HTTP port, so the CLI command can touch a file while connected (svc-agent --health-file=/run/svc-agent.ok) for an exec probe. Use it for readiness, not liveness.
  • Laravel. spacefix/sdk-laravel adds a service provider, a config/svc.php file and an Artisan command, php artisan svc:agent, which you run under Supervisor, never inside a request or a queue job. A Symfony bundle provides php bin/console svc:agent the same way.

Telemetry

An optional package reports process health to the Agents page, the counterpart of @spacefix/telemetry:

php
use Spacefix\Svc\Telemetry\Telemetry;

$agent->use(new Telemetry());                                         // every 10 seconds over the agent's connection
$agent->use(new Telemetry(intervalSeconds: 30, report: fn (array $m) => $statsd->gauge('heap', $m['heapUsed'])));
OptionDefaultMeaning
intervalSeconds10How often to sample and report
reportbuilt inA callable that receives each sample; replaces the gateway transport
oteltrueAlso publish through OpenTelemetry

OpenTelemetry

The bridge uses only the OpenTelemetry API (open-telemetry/api). It is a no-op until an OpenTelemetry SDK is registered, and it is off when the API package is not installed. Each call that reaches your agent produces a server span (tools/call <name>, resources/read <uri> or prompts/get <name>) that continues the caller's trace, plus the duration histogram mcp.server.operation.duration, the counter spacefix.agent.reconnects, the gauge spacefix.agent.connected and connection logs. Argument values and results are never recorded. PHP's OpenTelemetry metrics exporters are less mature than other languages', so spans and logs are the dependable signals. The attributes and metric names are the same in every SDK; see OpenTelemetry.

Next steps