> ## Documentation Index
> Fetch the complete documentation index at: https://docs.armature.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP

> Instrument a PHP MCP server with armature/mcp-analytics

The PHP SDK instruments servers built with the official PHP MCP SDK, [`mcp/sdk`](https://github.com/modelcontextprotocol/php-sdk) 0.7.x, on PHP 8.1 and newer. It captures sessions and tool calls without changing how your server behaves.

## Install

```bash theme={null}
composer require armature/mcp-analytics
```

## Set your API key

The SDK reads your configuration from the environment via `Config::fromEnvironment()`:

```bash theme={null}
ANALYTICS_INGEST_API_KEY=<your ingest key>
ANALYTICS_INGEST_URL=<your region's ingest URL>
```

Copy both values from the dashboard when you mint the key. US workspaces can omit `ANALYTICS_INGEST_URL` (the SDK defaults to the US endpoint); EU workspaces must set it to `https://eu.armature.tech/api/mcp-analytics/ingest`. If the key is missing, the SDK quietly no-ops — your server keeps working, and no data is sent.

## Instrument your server

Call `Analytics::instrument()` on the builder before adding or discovering tools:

```php server.php theme={null}
<?php

declare(strict_types=1);

use Armature\McpAnalytics\Analytics;
use Armature\McpAnalytics\Config;
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;

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

$builder = Server::builder()
    ->setServerInfo('Customer MCP', '1.0.0');

$analytics = Analytics::instrument(
    builder: $builder,
    config: Config::fromEnvironment(),
);

$builder->addTool(
    handler: static fn (string $customerId): array => [
        'customer_id' => $customerId,
        'status' => 'active',
    ],
    name: 'lookup_customer',
    description: 'Look up a customer.',
    inputSchema: [
        'type' => 'object',
        'properties' => [
            'customerId' => ['type' => 'string'],
        ],
        'required' => ['customerId'],
    ],
);

$server = $builder->build();

try {
    $server->run(new StdioTransport());
} finally {
    $analytics->close();
}
```

Manual tools, explicit `Builder::add(...)` definitions, custom loaders, and attribute discovery are all covered — the instrumentation decorates the official registry after definitions are finalized.

If your application already passes a custom `container`, `registry`, or `referenceHandler` to the builder, hand the same instances to `Analytics::instrument(...)` — otherwise the SDK installs its own and tools registered through yours could bypass the wrapper:

```php theme={null}
$analytics = Analytics::instrument(
    builder: $builder,
    config: Config::fromEnvironment(),
    container: $container,
    registry: $registry,
    referenceHandler: $referenceHandler,
);
```

Supplying a custom registry makes `mcp/sdk` 0.7 finalize builder loaders eagerly during `build()`; loader failures stay visible rather than being hidden.

## Streamable HTTP

Add the analytics PSR-15 middleware after your authentication middleware and keep the official transport defaults:

```php theme={null}
use Mcp\Server\Transport\StreamableHttpTransport;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;

$factory = new Psr17Factory();
$request = (new ServerRequestCreator($factory, $factory, $factory, $factory))
    ->fromGlobals();

$transport = new StreamableHttpTransport(
    request: $request,
    responseFactory: $factory,
    streamFactory: $factory,
    middleware: [
        ...StreamableHttpTransport::defaultMiddleware(),
        $authenticationMiddleware,
        $analytics->httpMiddleware(),
    ],
);

try {
    $response = $server->run($transport);
} finally {
    $analytics->flush();
}
```

`Server::run()` returns the PSR-7 response for HTTP transports — return `$response` from your framework handler, or pass it to your response emitter in a standalone script; nothing is sent to the client otherwise.

If your application doesn't already provide PSR-17 factories and a PSR-7 server-request creator, install one implementation: `composer require nyholm/psr7 nyholm/psr7-server`.

The middleware retains only the request headers and scalar authentication attributes needed for attribution. It never reads the request body or changes the request or response.

<Note>
  PHP is request-scoped by design, so delivery is **awaited by default** — events are sent before the response returns. That's the right mode for PHP-FPM, Lambda, and other serverless runtimes with no extra configuration.
</Note>

## Configuration

`Config::fromEnvironment()` is the only path that reads `ANALYTICS_INGEST_API_KEY` and `ANALYTICS_INGEST_URL` — the bare constructor defaults `apiKey` to `null`, which disables delivery. To customize options, carry the environment values over:

```php theme={null}
use Armature\McpAnalytics\Config;

$environment = Config::fromEnvironment();

$config = new Config(
    apiKey: $environment->apiKey,
    endpointUrl: $environment->endpointUrl,
    timeoutMs: 5000,
    onError: static function (Throwable $error, array $batch): void {
        // Log the safe error code/class. Do not log the batch.
    },
);
```

| Option              | Default                   | Description                                                                                                                                                                                                                                                               |
| ------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apiKey`            | `null`                    | API key used to authenticate ingestion. `Config::fromEnvironment()` fills it from `ANALYTICS_INGEST_API_KEY`; without a key (or a custom `emitter`) the SDK no-ops.                                                                                                       |
| `endpointUrl`       | US ingest endpoint        | `Config::fromEnvironment()` fills it from `ANALYTICS_INGEST_URL`; EU workspaces must use the EU endpoint.                                                                                                                                                                 |
| `enabled`           | `true`                    | Set `false` to disable capture entirely.                                                                                                                                                                                                                                  |
| `delivery`          | `DeliveryMode::Await`     | Sends before the response returns. `DeliveryMode::Deferred` is accepted only with a `scheduler`.                                                                                                                                                                          |
| `timeoutMs`         | `5000`                    | Delivery timeout in milliseconds.                                                                                                                                                                                                                                         |
| `actorId`           | Derived from request auth | A string or callable used to attribute calls to a user. See [Identifying users](/sdks/identifying-users).                                                                                                                                                                 |
| `actorIdentifier`   | `null`                    | Attach a caller-provided identifier verbatim. See [Identifying users](/sdks/identifying-users).                                                                                                                                                                           |
| `emitter`           | Network emitter           | Replace delivery with your own `EmitterInterface` — useful in tests.                                                                                                                                                                                                      |
| `onError`           | `null`                    | Called when delivery fails. Failures never reach your tools; log only the safe `DeliveryError` fields (`errorCode`, `status`, `retryable`, `attempts`, `causeClass`).                                                                                                     |
| `captureTelemetry`  | `true`                    | Set `false` to disable conversation-derived telemetry while keeping call analytics.                                                                                                                                                                                       |
| `requestCapability` | Auto                      | Injects the `request_capability` tool whenever a delivery path is configured (see [What the SDK captures](/sdks/telemetry#the-request_capability-tool)). Set `false` to disable; set `true` to reserve the name — a colliding customer tool then throws during `build()`. |
| `scheduler`         | `null`                    | `SchedulerInterface` required for `DeliveryMode::Deferred`; it must guarantee the scheduled task runs.                                                                                                                                                                    |
| `logger`            | PHP error log             | PSR-3 logger for configuration warnings. Warning context never includes tool arguments or credentials.                                                                                                                                                                    |

The redaction callbacks `redact` and `redactEvent` are documented in the [SDK README](https://github.com/armature-tech/mcp-analytics-php#readme). Two more options live next to them: `redactSecrets` (boolean, default `true`) turns built-in credential redaction off, and `telemetryFieldMap` maps existing tool arguments onto telemetry fields.

## Flush on shutdown

For long-running processes call `$analytics->close()` when the server exits (as in the stdio example above); in request-scoped runtimes call `$analytics->flush()` after the transport runs.

## Verify locally

The language-independent doctor can inspect a running server, verify the telemetry contract on every tool, and authenticate your ingest key. The doctor is Node-based and, outside a JavaScript project, its MCP client package must be listed explicitly (it is an optional peer dependency, so bare `npx @armature-tech/mcp-analytics doctor` fails with `ERR_MODULE_NOT_FOUND`):

```bash theme={null}
npx --yes --package=@armature-tech/mcp-analytics \
  --package=@modelcontextprotocol/sdk \
  mcp-analytics doctor --url http://localhost:3000/mcp
```

Use `--skip-ingest` for an offline schema check and `--json` for machine-readable output.

## Troubleshooting

<AccordionGroup>
  <Accordion title="`addTool()` rejects my callable">
    `mcp/sdk` 0.7 accepts closures, class/method pairs, and invokable class strings — not invokable object instances. Adapt a bare named function or invokable object with `Closure::fromCallable(...)` before passing it to `addTool()`.
  </Accordion>

  <Accordion title="Streamable HTTP can't find a response factory">
    Keep your application's existing PSR-17 implementation, or install the Nyholm packages shown above, then pass the factories explicitly to the transport.
  </Accordion>

  <Accordion title="Composer rejects the dependency">
    The SDK supports PHP 8.1+ and `mcp/sdk >=0.7.0 <0.8.0`. Upgrade or constrain your application explicitly rather than bypassing Composer's platform checks.
  </Accordion>

  <Accordion title="No data arrives">
    Check that `ANALYTICS_INGEST_API_KEY` is set in the server's environment (missing keys no-op silently by design), that `ANALYTICS_INGEST_URL` matches your workspace region — EU keys are rejected by the US endpoint — and pass an `onError` callback to surface delivery failures. If you construct `Config` yourself, remember the bare constructor never reads environment variables — start from `Config::fromEnvironment()`.
  </Accordion>
</AccordionGroup>
