PHP SDK 첫 서버 만들기

PHP SDK 첫 서버 만들기

MCP 서버는 사실 서너 줄의 연결 코드와 함께 있는 평범한 PHP 클래스일 뿐이에요. vendor/ 디렉토리 옆에 server.php를 만들면 첫 서버가 완성됩니다.

출처: First server - MCP PHP SDK

server.php

#!/usr/bin/env php
<?php

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

use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;

class Calculator
{
    /**
     * Adds two numbers.
     */
    #[McpTool]
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }

    #[McpResource(uri: 'config://calculator/settings')]
    public function settings(): array
    {
        return ['precision' => 2];
    }
}

exit(Server::builder()
    ->setServerInfo('Calculator', '1.0.0')
    ->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor'])
    ->build()
    ->run(new StdioTransport()));

탐색은 symfony/finder가 필요해요:

composer require symfony/finder

각 부분이 하는 일

#[McpTool]은 메서드를 모델이 호출할 수 있는 액션으로 표시해요. 이름은 메서드 이름에서, 설명은 docblock(요약 + 더 긴 설명을 쓰면 그것까지)에서, 입력 스키마는 파라미터 타입에서 생성돼요 — int $a, int $b는 필수 정수 두 개를 가진 JSON Schema가 됩니다. Tools 참고

#[McpResource]은 메서드를 애플리케이션이 URI로 읽을 수 있는 읽기 전용 데이터로 표시해요. Resources 참고

setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor'])는 그 디렉토리들에서 속성이 붙은 클래스를 스캔해요. 스캔은 lazy해서 첫 요청이 레지스트리를 필요로 할 때( build() 반환 시점이 아니라) 발생합니다 — 미리 비용을 지불하고 싶다면 setLazyLoading(false)를 호출하세요. vendor를 제외하는 게 중요한 이유는 스캔이 재귀적이라, 그대로 두면 의존성이 실어 나르는 모든 파일을 읽고 오토로드하기 때문이에요. 요소를 명시적으로 등록하고 싶다면(또는 둘을 섞으려면) Registering elements를 참고하세요.

run(new StdioTransport())는 stdin/stdout 위에서 JSON-RPC를 주고받고 exit 코드를 반환해요. 이는 로컬 MCP 호스트가 서브프로세스로 실행하는 트랜스포트이며, 웹에 노출할 서버라면 대신 HTTP 트랜스포트를 쓰세요.

절대 STDOUT에 쓰지 마세요 — STDIO 트랜스포트에서는 STDOUT이 곧 프로토콜이에요. 핸들러에서 echo, print_r(), 어중간한 var_dump()가 스트림을 깨뜨립니다. STDERR에 쓰거나 logger를 사용하세요.

실행하기

php server.php

아무 일도 일어나지 않아요 — 서버가 stdin에서 JSON-RPC를 기다리는 거라서, 정확히 맞는 동작이에요. Ctrl+C로 멈추고, 진짜 클라이언트가 대신 실행하게 하세요: Inspector로 시험해 보기.

더 알아보기 (Learn more)