feat: trips-only chatbot MCP endpoint without editorial website text

This commit is contained in:
fromme committed 2026-10-12 14:43:46 +02:00
1 parent 9f805aaec7
commit 26d4140c18
7 files changed
+110 -11

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Chatbot\Mcp\TripsOnly;
use App\Chatbot\Mcp\ToolCallRunner;
use App\Chatbot\TripCatalog;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\Schema;
use Mcp\Exception\ToolCallException;
use Mcp\Schema\Result\CallToolResult;
use Mcp\Schema\ToolAnnotations;
/**
* get_trip of the trips-only server (/mcp/trips): the same trip without the product and
* hotel page texts, whose prices and inclusions can contradict the booking system.
*/
#[McpTool(
name: 'get_trip',
description: 'Full details of one trip: destination, hotels with their facts, all upcoming dates with rooms, prices per person in EUR and availability, pickup points and departure times. Pickup surcharges are per person and per direction: surchargeOutbound for the way there, surchargeReturn for the way back, surchargeRoundTrip for both; quote the round trip unless the user travels one way only. Use after search_trips, or when the user names a specific trip or a booked date (date codes such as SSLMP061226 narrow the answer to that date). Prices and availability are a snapshot as of dataAsOf; the booking page is binding. For anything this does not answer, refer the user to the trip\'s url.',
annotations: new ToolAnnotations(readOnlyHint: true, openWorldHint: false),
)]
final class GetTripTool
{
public function __construct(
private readonly TripCatalog $catalog,
private readonly ToolCallRunner $runner,
) {
}
public function __invoke(
#[Schema(description: 'Trip code as returned by search_trips, or a date code (dateCode) for one date of a trip.', minLength: 1)]
string $code,
#[Schema(description: 'Narrows the dates to one hotel when a trip has several.')]
?string $hotelCode = null,
): CallToolResult {
return $this->runner->run('get_trip', get_defined_vars(), function () use ($code, $hotelCode): array {
$trip = $this->catalog->get(trim($code), $hotelCode, editorial: false);
if (null === $trip) {
throw new ToolCallException(sprintf('No trip or date with code "%s". Use search_trips to find trip codes.', $code));
}
return $trip;
});
}
}
+8 -6
View File
@@ -126,9 +126,12 @@ class TripCatalog
* booking documents (SSLMP061226 is product SSLMP on 06.12.26); it narrows the trip to
* that date, shown even when it has already started.
*
* Without $editorial the product and hotel page texts are left out, and only what is
* structured remains: names, links, hotel facts and everything from the booking system.
*
* @return array<string, mixed>|null
*/
public function get(string $code, ?string $hotelCode = null): ?array
public function get(string $code, ?string $hotelCode = null, bool $editorial = true): ?array
{
$catalog = $this->load();
$code = (new DateCodeUtility())->sanitize(trim($code));
@@ -175,8 +178,8 @@ class TripCatalog
static fn (ContentDocument $other): array => ['site' => $other->getSite(), 'url' => $other->getUrl()],
array_filter(array_slice($products, 1), static fn (ContentDocument $other): bool => null !== $other->getUrl()),
)),
'description' => $this->truncate($product->getBodyMd(), self::BODY_LIMIT),
'hotels' => $this->hotelDetails($catalog, $hotelCodes, $product),
] + ($editorial ? ['description' => $this->truncate($product->getBodyMd(), self::BODY_LIMIT)] : []) + [
'hotels' => $this->hotelDetails($catalog, $hotelCodes, $product, $editorial),
'dates' => $this->dates($catalog, $rows),
'dataAsOf' => $this->dataAsOf($catalog['export']),
];
@@ -495,7 +498,7 @@ class TripCatalog
*
* @return list<array<string, mixed>>
*/
private function hotelDetails(array $catalog, array $hotelCodes, ContentDocument $product): array
private function hotelDetails(array $catalog, array $hotelCodes, ContentDocument $product, bool $editorial): array
{
$hotels = [];
@@ -521,8 +524,7 @@ class TripCatalog
'name' => $hotel->getTitle(),
'url' => $hotel->getUrl(),
'facts' => is_array($hotel->getData()['facts'] ?? null) ? array_filter($hotel->getData()['facts'], static fn (mixed $value): bool => null !== $value && '' !== $value) : [],
'description' => $this->truncate($hotel->getBodyMd(), self::HOTEL_BODY_LIMIT),
], $hotels));
] + ($editorial ? ['description' => $this->truncate($hotel->getBodyMd(), self::HOTEL_BODY_LIMIT)] : []), $hotels));
}
/**