mcp/sdk 0.7.x, on PHP 8.1 and newer. It captures sessions and tool calls without changing how your server behaves.
Install
Set your API key
The SDK reads your configuration from the environment viaConfig::fromEnvironment():
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
CallAnalytics::instrument() on the builder before adding or discovering tools:
server.php
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:
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: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.
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.
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:
The redaction callbacks
redact and redactEvent are documented in the SDK 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 barenpx @armature-tech/mcp-analytics doctor fails with ERR_MODULE_NOT_FOUND):
--skip-ingest for an offline schema check and --json for machine-readable output.
Troubleshooting
`addTool()` rejects my callable
`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().Streamable HTTP can't find a response factory
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.
Composer rejects the dependency
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.No data arrives
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().
