MCP
Overview
The Model Context Protocol is an open standard that lets AI clients, such as Claude or Cursor, interact with your application through tools, prompts and resources.
Tempest ships with an MCP server framework in tempest/mcp. Servers are plain classes annotated with McpServer, and their capabilities are methods annotated with McpTool, McpPrompt or McpResource. Both are registered through discovery.
Defining a server
An MCP server is a class with the McpServer attribute. Its name is derived from the class name and its version defaults to 0.0.1, unless specified in the attribute. When a path is provided, the server is exposed over HTTP; every server is also accessible through the mcp:serve command.
use Tempest\Mcp\McpServer; use Tempest\Mcp\McpTool; #[McpServer(path: '/mcp')] final class DemoServer { #[McpTool(description: 'Adds two numbers')] public function add(int $a, int $b): int { return $a + $b; } }
Tool, prompt and resource methods defined inside an #[McpServer] class are automatically attached to that server. This includes public methods inherited from parent classes, so servers may share primitives through a common base class, which may be abstract. Methods in other classes may attach themselves to a server by specifying the server parameter:
use Tempest\Mcp\McpTool; final readonly class OrderTools { #[McpTool(server: DemoServer::class)] public function countOrders(): int { // … } }
Handler classes and their methods are resolved through the container, so dependencies may be injected in the constructor or directly as method parameters.
Tools
Tools are methods annotated with McpTool. The tool name defaults to the snake-cased method name, and the input schema is derived from the method signature.
Scalar parameters, backed enums and arrays become schema properties; parameters with default values or nullable types become optional; everything else is injected by the container and excluded from the schema.
use Tempest\Mcp\Description; use Tempest\Mcp\McpTool; use Tempest\Validation\Rules\IsBetween; #[McpTool(description: 'Rates a product')] public function rate( ProductRepository $products, // injected, not part of the schema #[Description('The identifier of the product')] string $product, #[IsBetween(min: 1, max: 5)] int $rating, ): string { // … }
Validation attributes from tempest/validation double as schema constraints and are enforced at call time. For instance, #[IsBetween] becomes minimum and maximum, #[HasLength] becomes minLength and maxLength, and #[IsIn] becomes an enum. When validation fails, the client receives an error result containing the validation messages.
Return values are normalized to MCP content:
- A
stringor other scalar becomes text content; - An associative array or object becomes JSON text alongside
structuredContent; - Instances of
Text,Image,AudioandResourceLink, or lists of them, are passed through as-is.
Exceptions thrown by a tool handler are reported to the exception handler and returned to the client as an error result.
Prompts
Prompts are methods annotated with McpPrompt. Their parameters are advertised as prompt arguments, and the return value becomes the prompt messages.
use Tempest\Mcp\McpPrompt; #[McpPrompt(description: 'Generates a code review prompt')] public function reviewCode(string $code): string { return "Review the following code: {$code}"; }
Resources
Resources are methods annotated with McpResource, which requires a uri. A URI may contain template variables that correspond to method parameters, in which case the resource is advertised as a resource template and matched dynamically when read.
use Tempest\Mcp\Content\Blob; use Tempest\Mcp\McpResource; #[McpResource(uri: 'app://config', mimeType: 'application/json')] public function config(): array { return ['env' => 'production']; } #[McpResource(uri: 'app://users/{id}')] public function user(int $id): string { return "User #{$id}"; }
String return values become text resource contents, while Blob instances become binary blob contents.
Some MCP clients only request the standard resource list and do not request resource templates. To also include resource templates in resources/list, create an mcp.config.php file:
use Tempest\Mcp\McpConfig; return new McpConfig( listResourceTemplatesAsResources: true, );
Resource templates will remain available through resources/templates/list.
Transports
HTTP
When McpServer specifies a path, a POST route is registered automatically. The HTTP transport is stateless: each request receives a single JSON response, notifications are acknowledged with 202 Accepted, and other HTTP methods are answered with 405 Method Not Allowed. You may protect the route like any other by adding middleware through a route decorator, or by placing the server behind your existing authentication middleware.
Standard input/output
Every discovered server can be served over standard input and output, which is the transport most local MCP clients use:
# Serve a server by its name ./tempest mcp:serve demo-server
Use mcp:list to see all discovered servers, their transports, and how many tools, prompts and resources they expose.
Testing
The framework provides a mcp property in tests that extend IntegrationTest. It drives the protocol in-process and performs the initialization handshake.
public function test_add(): void { $this->mcp ->onServer(DemoServer::class) ->callTool('add', ['a' => 1, 'b' => 2]) ->assertOk() ->assertText('3'); }
The connection object provides callTool(), getPrompt(), readResource(), listTools(), listResources(), listPrompts(), and a generic send() method for raw protocol requests. Responses may be asserted with assertOk(), assertError(), assertText(), assertTextContains(), assertStructured(), assertToolListed() and assertSee().