PHP code example of rasuvaeff / yii3-mcp

1. Go to this page and download the library: Download rasuvaeff/yii3-mcp library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

rasuvaeff / yii3-mcp example snippets


use Mcp\Capability\Attribute\McpTool;

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

    /**
     * Returns the current status of an order.
     */
    #[McpTool(name: 'order.status')]
    public function status(string $orderId): string
    {
        return $this->orders->get($orderId)->status->value;
    }
}

/**
 * @return array{status: string, total: int}
 */
#[McpTool(
    name: 'order.status',
    outputSchema: [
        'type' => 'object',
        'properties' => [
            'status' => ['type' => 'string'],
            'total' => ['type' => 'integer'],
        ],
        '

use Mcp\Schema\ToolAnnotations;

#[McpTool(
    name: 'order.cancel',
    annotations: new ToolAnnotations(
        readOnlyHint: false,
        destructiveHint: true,
        idempotentHint: true,
        openWorldHint: false,
    ),
)]
public function cancel(string $orderId): string
{
    $this->orders->cancel($orderId);

    return 'cancelled';
}

use Mcp\Schema\Elicitation\BooleanSchemaDefinition;
use Mcp\Schema\Elicitation\ElicitationSchema;
use Mcp\Server\RequestContext;

#[McpTool(
    name: 'release.deploy',
    annotations: new ToolAnnotations(
        readOnlyHint: false,
        destructiveHint: true,
        idempotentHint: false,
        openWorldHint: false,
    ),
)]
public function deploy(string $version, RequestContext $context): string
{
    $client = $context->getClientGateway();
    $client->progress(progress: 1, total: 2, message: 'Validation complete');

    if (!$client->supportsElicitation()) {
        throw new RuntimeException('Client does not support Deployment %s queued', $version);
}

final readonly class BetaTools implements ConditionalToolInterface
{
    public function __construct(private FeatureFlags $flags) {}

    public function shouldRegister(): bool
    {
        return $this->flags->isEnabled('mcp-beta-tools');
    }

    #[McpTool(name: 'beta.op')]
    public function betaOp(): string { ... }
}

// config/params.php
return [
    'rasuvaeff/yii3-mcp' => [
        'server_name' => 'my-app',
        'server_version' => '1.0.0',
        'tools' => [OrderTools::class],
        'endpoint_secret' => getenv('MCP_SECRET'),
    ],
];

// config/routes.php
Route::methods(['POST', 'GET', 'DELETE', 'OPTIONS'], '/mcp')
    ->middleware(SharedSecretMiddleware::class)
    ->action(McpAction::class),

// add McpServeCommand to your console commands
./yii mcp:serve

// add McpListCommand to your console commands
./yii mcp:list
./yii mcp:list --json   # full definitions as normalized JSON

// config/common/di/mcp.php
use Mcp\Server\Session\Psr16SessionStore;
use Mcp\Server\Session\SessionStoreInterface;

return [
    SessionStoreInterface::class => static fn (CacheInterface $cache) =>
        new Psr16SessionStore($cache),
];

'rasuvaeff/yii3-mcp' => [
    'prompts_path' => __DIR__ . '/../resources/prompts',
],

use Mcp\Capability\Attribute\CompletionProvider;

#[McpPrompt(name: 'review')]
public function review(
    #[CompletionProvider(values: ['security', 'performance'])] string $focus,
    #[CompletionProvider(enum: Environment::class)] string $environment,
): string { /* … */ }

#[McpResourceTemplate(uriTemplate: 'app://reports/{region}', name: 'report')]
public function report(
    #[CompletionProvider(provider: RegionCompletionProvider::class)] string $region,
): string { /* … */ }

'rasuvaeff/yii3-mcp' => [
    // free-form "how to use this server" text served in the initialize result —
    // the agent reads it before its first call. Empty = omitted.
    'instructions' => 'Prefer order.status over reading app://orders/{id}.',
    // page size for tools/resources/templates/prompts lists. Applies to the
    // SDK's handlers AND this package's filtering ones, so they can never page
    // differently depending on whether visibility is configured.
    'pagination_limit' => 50,
    // pins the revision advertised in initialize; empty keeps the SDK's default
    // (2025-11-25). An unsupported value fails at config load, not at runtime.
    'protocol_version' => '',
],

public function __construct(private ResourceUpdateNotifier $notifier) {}

#[McpTool(name: 'order.cancel')]
public function cancel(string $orderId, RequestContext $context): string
{
    $this->orders->cancel($orderId);
    $this->notifier->notify($context, 'app://orders/' . $orderId);

    return 'cancelled';
}

use Mcp\Server\Session\FileSessionStore;
use Rasuvaeff\Yii3Mcp\{McpAction, McpServerFactory, SharedSecretMiddleware};

// any PSR-11 container — Yii3's, PHP-DI, Laravel's, a hand-rolled one
$container = /* ... */;

$sessionStore = new FileSessionStore(directory: sys_get_temp_dir() . '/mcp-sessions', ttl: 3600);
$factory = new McpServerFactory(container: $container, sessionStore: $sessionStore, name: 'my-app', version: '1.0.0');
$server = $factory->create([OrderTools::class]);

$psr17 = /* any PSR-17 factory, e.g. nyholm/psr7 or guzzlehttp/psr7 */;
$action = new McpAction(server: $server, responseFactory: $psr17, streamFactory: $psr17);
$middleware = new SharedSecretMiddleware(secret: getenv('MCP_SECRET'), responseFactory: $psr17);

// route POST/GET/DELETE/OPTIONS /mcp through $middleware -> $action
// in whatever middleware-dispatch shape the framework's router expects

use Rasuvaeff\Yii3Mcp\Interceptor\ToolCallContext;
use Rasuvaeff\Yii3Mcp\Interceptor\ToolCallInterceptorInterface;

final readonly class TracingInterceptor implements ToolCallInterceptorInterface
{
    public function __construct(private LoggerInterface $logger) {}

    public function intercept(ToolCallContext $context, callable $next): mixed
    {
        // $context->toolName, $context->arguments, $context->session,
        // $context->getClientInfo() — who is calling what with which input
        $this->logger->info('tools/call', ['tool' => $context->toolName]);

        return $next();   // skip $next() to short-circuit
    }
}

// config/params.php — resolved through the container, first = outermost
'rasuvaeff/yii3-mcp' => [
    'interceptors' => [TracingInterceptor::class],
],

use Rasuvaeff\Yii3Mcp\Interceptor\ArgumentMasker;

$masker = new ArgumentMasker();                       // or: new ArgumentMasker(['ssn', 'password'])
$safe = $masker->mask($context->arguments);
// ['user' => ['name' => 'alice', 'password' => '***']]

$this->logger->info('tools/call', ['tool' => $context->toolName, 'arguments' => $safe]);

'rasuvaeff/yii3-mcp' => [
    'session' => ['budget' => 50],   // 0 = unlimited (default)
],

'rasuvaeff/yii3-mcp' => [
    'limits' => ['tool_result_bytes' => 0],   // 0 = unlimited (default)
],

'rasuvaeff/yii3-mcp' => [
    'cache' => [
        'tools' => ['blog_tags_list' => 60],
    ],
],

'rasuvaeff/yii3-mcp' => [
    'client_secrets' => [
        'ci' => getenv('MCP_SECRET_CI'),
        'claude' => [getenv('MCP_SECRET_CLAUDE_OLD'), getenv('MCP_SECRET_CLAUDE_NEW')],
    ],
],

final readonly class AppToolCallLimiter implements ToolCallLimiterInterface
{
    public function __construct(private CounterInterface $counter) {}

    public function allow(?string $clientId, string $toolName): bool
    {
        return $this->counter->hit(($clientId ?? 'no-client') . ':' . $toolName)->isAllowed();
    }
}

// params
'rasuvaeff/yii3-mcp' => [
    'interceptors' => [RateLimitInterceptor::class],
],
// di: bind ToolCallLimiterInterface => AppToolCallLimiter

use Rasuvaeff\Retry\Retry;
use Rasuvaeff\Yii3Mcp\Interceptor\ToolCallContext;
use Rasuvaeff\Yii3Mcp\Interceptor\ToolCallInterceptorInterface;
use Rasuvaeff\Yii3Mcp\OpenApi\Exception\OperationFailedException;

final readonly class RetryInterceptor implements ToolCallInterceptorInterface
{
    /** @param list<string> $idempotentTools verified idempotent — never blanket-retry */
    public function __construct(private array $idempotentTools) {}

    public function intercept(ToolCallContext $context, callable $next): mixed
    {
        if (!in_array($context->toolName, $this->idempotentTools, true)) {
            return $next();
        }

        return Retry::new()
            ->maxAttempts(3)
            ->withExponential(baseMs: 100, multiplier: 2.0, capMs: 2_000)
            ->retryOn(OperationFailedException::class)   // transient failures only
            ->run($next);
    }
}

'rasuvaeff/yii3-mcp' => [
    'visibility' => [
        'deny' => ['admin.*'],        // hide matches
        'allow' => [],                // non-empty = hide everything it does not match
    ],
],

use Mcp\Schema\Tool;
use Mcp\Server\Session\SessionInterface;
use Rasuvaeff\Yii3Mcp\Visibility\ToolVisibilityInterface;

final readonly class PlanBasedVisibility implements ToolVisibilityInterface
{
    public function isVisible(Tool $tool, ?SessionInterface $session): bool
    {
        // decide from $session->get('client_info'), tenant data, …
        return !str_starts_with($tool->name, 'admin.') || $this->isAdmin($session);
    }
}

'rasuvaeff/yii3-mcp' => [
    'tool_visibility' => PlanBasedVisibility::class,   // DI-resolved
],

// config/params.php — each list resolved through the container, first = outermost
'rasuvaeff/yii3-mcp' => [
    'prompt_interceptors' => [PromptAuditInterceptor::class],     // Interceptor\PromptGetInterceptorInterface
    'resource_interceptors' => [ResourceAclInterceptor::class],   // Interceptor\ResourceReadInterceptorInterface
    'prompt_visibility' => PlanBasedPromptVisibility::class,      // Visibility\PromptVisibilityInterface
    'resource_visibility' => PlanBasedResourceVisibility::class,  // Visibility\ResourceVisibilityInterface
],

'rasuvaeff/yii3-mcp' => [
    'configurators' => [MyServerConfigurator::class],   // DI-resolved
],

final readonly class MyServerConfigurator implements ServerConfiguratorInterface
{
    #[\Override]
    public function configure(Builder $builder): void
    {
        // $builder->addTool(...) / addResource(...) / addPrompt(...) …
    }
}

// config/routes.php — secret first (fail-closed), then tenant, then MCP
Route::methods(['POST', 'GET', 'DELETE', 'OPTIONS'], '/mcp')
    ->middleware(SharedSecretMiddleware::class)
    ->middleware(TenantResolutionMiddleware::class)   // e.g. HeaderTenantResolver('X-Tenant-Id')
    ->action(McpAction::class),

// an MCP client carries both headers
"headers": { "X-Mcp-Secret": "...", "X-Tenant-Id": "acme" }

// config/common/di/mcp.php
SessionStoreInterface::class => static fn (CurrentTenant $tenant) =>
    new FileSessionStore(
        directory: sys_get_temp_dir() . '/mcp-sessions/' . $tenant->get()->getId(),
    ),

// config/params.php
'rasuvaeff/yii3-mcp' => [
    'openapi' => [
        // file path OR http(s) URL — e.g. the app's own spec endpoint,
        // always current; fetched with `spec_headers`, NOT with `headers`
        'spec_path' => 'https://api.example.com/rest/json-url',
        'base_url' => 'https://api.example.com',
        'operations' => ['getBlogTags', 'getPage'],   // allow-list, empty = nothing
        // rename an ugly generated operationId into an LLM-friendly tool
        // name; unmapped operations keep their operationId
        'tool_names' => ['getBlogTags' => 'blog_tags_list'],
        // operation-call credentials, sent to base_url only
        'headers' => ['Authorization' => 'Bearer ' . getenv('MCP_API_TOKEN')],
        // spec-fetch credentials, sent to spec_path only (empty by default)
        'spec_headers' => [],
        'cache_ttl' => 60,             // PSR-16 URL-spec cache; 0 = fetch every build
        'safe_methods_only' => true,   // read-only bridge: non-GET in the list => build error
        'max_response_bytes' => 4_194_304, // upstream body cap, read incrementally
        'opaque_errors' => false,      // true = suppress upstream error bodies
    ],
],

'rasuvaeff/yii3-mcp' => [
    'openapi' => [
        'operation_modifier' => MyOperationModifier::class,   // DI-resolved
    ],
],

use Mcp\Schema\Tool;
use Rasuvaeff\Yii3Mcp\OpenApi\Operation;
use Rasuvaeff\Yii3Mcp\OpenApi\OperationModifierInterface;

final readonly class MyOperationModifier implements OperationModifierInterface
{
    public function modify(Operation $operation, Tool $tool): Tool
    {
        return new Tool(
            name: $tool->name,
            title: $tool->title,
            inputSchema: $tool->inputSchema,
            description: $tool->description . ' (read-only bridge)',
            annotations: $tool->annotations,
            outputSchema: $tool->outputSchema,
        );
    }
}

'rasuvaeff/yii3-mcp' => [
    'openapi' => [
        // operationIds that get an extra `dryRun` boolean argument
        'dry_run' => ['createSubscriber'],
    ],
],

'rasuvaeff/yii3-mcp' => [
    'apps' => [
        // announce the extension (enough for attribute-based apps)
        'enable' => true,
        // declarative apps — no PHP class needed
        'definitions' => [
            [
                'uri' => 'ui://dashboard',        // omains' => ['api.example.com']],
                'permissions' => ['geolocation' => true],
                'prefers_border' => true,
            ],
        ],
    ],
],

#[McpResource(
    uri: 'ui://report',
    name: 'report',
    mimeType: McpApps::MIME_TYPE,
    meta: ['ui' => new \stdClass()],        // descriptor marker
)]
public function report(): TextResourceContents
{
    return new TextResourceContents(
        uri: 'ui://report',
        mimeType: McpApps::MIME_TYPE,
        text: '<!DOCTYPE html><h1>Report</h1>',
        meta: ['ui' => new UiResourceContentMeta(  // sandbox contract
            csp: new UiResourceCsp(connectDomains: ['api.example.com']),
            prefersBorder: true,
        )],
    );
}

#[McpTool(
    name: 'refresh_report',
    meta: ['ui' => new UiToolMeta(resourceUri: 'ui://report')],
)]
public function refresh(): string { /* … */ }

$tester = new McpTester($server, $psr17, $psr17, $psr17);

$result = $tester->callTool('order.status', ['orderId' => '42']);
$this->assertSame('paid', $result['content'][0]['text']);

$tester->listTools();                 // every paginated tool definition
$tester->listResources();             // every resource definition
$tester->listResourceTemplates();     // every resource-template definition
$tester->listPrompts();               // every prompt definition
$tester->readResource('app://x');     // resource contents
$tester->request('custom/method');     // any raw JSON-RPC method

SchemaSnapshot::verify($tester, __DIR__ . '/mcp-schema.json');
// a mismatch throws with a per-section summary:
// "tools: changed [order.status]; prompts: added [code-review]"