Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ reference concrete extension types.
## Conventions

- Throw `Mcp\Exception\*` (all implement `ExceptionInterface`), never native SPL exceptions.
- No `suggest` in `composer.json`: code that needs an optional package or extension checks for it and throws an
exception that names what to install.
- Add an `@author` tag to newly introduced classes.
- Define array shapes for parameters and return types.
- Prefer classic `if` statements over short-circuit evaluation when possible.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ All notable changes to `mcp/sdk` will be documented in this file.

* [BC Break] An `x-mcp-header` annotation that is not on a property reached through `properties` alone, e.g. under `items`, `anyOf` or `$defs`, makes the tool definition invalid instead of being ignored: `Tool` throws an `InvalidArgumentException` for it, the client drops such a tool from `tools/list` and refuses a sampling request that carries one. A null annotation is invalid too, wherever it sits.
* [BC Break] `CallToolResult` preserves explicit `structuredContent: null` in construction and decoding. Calls that used null for omission must omit the argument (use `meta: $meta` for metadata) or pass `StructuredContent::Absent`. An explicit null that violates the output schema becomes a tool error, and strict handshake-era clients can reject it. `hasStructuredContent()` distinguishes absent content from null; text-only `success()` and `error()` results remain absent.
* `ext-fileinfo` is optional: without it, creating content from a file without a MIME type throws a `LogicException`.

0.9.0
-----
Expand Down
7 changes: 0 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
],
"require": {
"php": "^8.2",
"ext-fileinfo": "*",
"opis/json-schema": "^2.4",
"php-http/discovery": "^1.20",
"phpdocumentor/reflection-docblock": "^6.0",
Expand All @@ -35,12 +34,6 @@
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0"
},
"suggest": {
"firebase/php-jwt": "Required for the JwtTokenValidator, and for private_key_jwt client authentication in the OAuth client.",
"psr/cache-implementation": "Required for JwtTokenValidator::fromIssuer() to cache metadata and keys.",
"symfony/finder": "Required for file-based discovery.",
"symfony/lock": "Required for the SymfonyLockSessionLock."
},
"require-dev": {
"ext-openssl": "*",
"amphp/amp": "^3.0",
Expand Down
6 changes: 4 additions & 2 deletions docs/get-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ The SDK ships as a single Composer package:
composer require mcp/sdk
```

It requires **PHP 8.2+** and the `fileinfo` extension. Most of what it pulls in is PSR
It requires **PHP 8.2+**. Most of what it pulls in is PSR
interface packages (`psr/container`, `psr/log`, `psr/http-message`, …); the rest is
`opis/json-schema` for schema validation, `symfony/uid` for session identifiers,
`phpdocumentor/reflection-docblock` for reading descriptions out of your docblocks,
`php-http/discovery` for finding PSR-17/PSR-18 implementations, and
`symfony/deprecation-contracts` for deprecation notices.
`symfony/deprecation-contracts` for deprecation notices. Without the `fileinfo` extension,
pass a MIME type to `ImageContent::fromFile()` and the other content factories that read
a file, or they throw an exception.

That is enough for a complete [STDIO server](../run/stdio.md) and for the
[STDIO client](../client/transports.md).
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Content/AudioContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static function fromFile(string $path, ?string $mimeType = null, ?Annotat
throw new RuntimeException(\sprintf('Could not read audio file: "%s".', $path));
}
$data = base64_encode($content);
$detectedMime = $mimeType ?? mime_content_type($path) ?: 'application/octet-stream';
$detectedMime = $mimeType ?? MimeType::guess($path) ?? 'application/octet-stream';

return new self($data, $detectedMime, $annotations);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Content/BlobResourceContents.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public static function fromStream(string $uri, $stream, string $mimeType, ?array
* */
public static function fromSplFileInfo(string $uri, \SplFileInfo $file, ?string $explicitMimeType = null, ?array $meta = null): self
{
$mimeType = $explicitMimeType ?? (mime_content_type($file->getPathname()) ?: null);
$mimeType = $explicitMimeType ?? MimeType::guess($file->getPathname());
$blob = file_get_contents($file->getPathname());
if (false === $blob) {
throw new InvalidArgumentException(\sprintf('Could not read file: "%s".', $file->getPathname()));
Expand Down
4 changes: 2 additions & 2 deletions src/Schema/Content/EmbeddedResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public static function fromFile(string $uri, string $path, ?string $explicitMime
throw new RuntimeException(\sprintf('Could not read file: "%s".', $path));
}

$guessedMimeType = $explicitMimeType ?? mime_content_type($path) ?: 'application/octet-stream';
$guessedMimeType = $explicitMimeType ?? MimeType::guess($path) ?? 'application/octet-stream';

if (self::isTextMimeTypeHeuristic($guessedMimeType) && mb_check_encoding($content, 'UTF-8')) {
$resourceContent = new TextResourceContents($uri, $guessedMimeType, $content);
Expand Down Expand Up @@ -121,7 +121,7 @@ public static function fromSplFileInfo(string $uri, \SplFileInfo $file, ?string
throw new RuntimeException(\sprintf('Could not read file: "%s".', $file->getPathname()));
}

return new self(new BlobResourceContents($uri, $explicitMimeType ?? (mime_content_type($file->getPathname()) ?: null), base64_encode($content)), $annotations);
return new self(new BlobResourceContents($uri, $explicitMimeType ?? MimeType::guess($file->getPathname()), base64_encode($content)), $annotations);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Content/ImageContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public static function fromFile(string $path, ?string $mimeType = null, ?Annotat
}

$data = base64_encode($contents);
$detectedMime = $mimeType ?? mime_content_type($path) ?: 'image/png';
$detectedMime = $mimeType ?? MimeType::guess($path) ?? 'image/png';

return new self($data, $detectedMime, $annotations);
}
Expand Down
36 changes: 36 additions & 0 deletions src/Schema/Content/MimeType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Schema\Content;

use Mcp\Exception\LogicException;

/**
* @internal
*
* @author Christopher Hertel <[email protected]>
*/
final class MimeType
{
/**
* @throws LogicException if the fileinfo extension is missing
*/
public static function guess(string $path): ?string
{
if (!\function_exists('mime_content_type')) {
throw new LogicException(\sprintf('Detecting the MIME type of "%s" requires the "fileinfo" PHP extension. Install it or pass the MIME type explicitly.', $path));
Comment on lines +28 to +29
}

$mimeType = mime_content_type($path);

return false === $mimeType ? null : $mimeType;
}
}
25 changes: 25 additions & 0 deletions tests/Unit/Schema/Content/MimeTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Tests\Unit\Schema\Content;

use Mcp\Schema\Content\MimeType;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\TestCase;

final class MimeTypeTest extends TestCase
{
#[RequiresPhpExtension('fileinfo')]
public function testGuessDetectsMimeTypeOfFile(): void
{
$this->assertSame('text/plain', MimeType::guess(__DIR__.'/../../../../LICENSE'));
}
}
Loading