Implementiere Marktmanagement-MCP-API

This commit is contained in:
gitea_admin committed 2026-09-19 08:45:47 +02:00
1 parent 2607f448ad
commit df5a533983
4 files changed
+622 -5

No files matched your search

@@ -47,12 +47,16 @@ OTC-Verkäufen abgeleitet.
## 4. Authentifizierung und Autorisierung
- MCP-Endpunkt über geschütztes HTTPS
- OAuth-/OIDC-basierte Benutzerautorisierung
- Token mit Benutzeridentität, Zielgruppe und Modul-Scope
- getrennte Scopes für Lesen, Schreiben und Archivieren
- Prüfung an der Transportgrenze
- Bearer-Token-Prüfung an der Transportgrenze
- der konfigurierte Token identifiziert den technischen Hermes-Benutzerkontext
- zusätzliche Prüfung im jeweiligen Tool-Handler
- keine langlebigen gemeinsamen Zugangsdaten
- kein direkter Datenbankzugriff für Hermes
Für die erste Implementierungsstufe wird bewusst keine neue OAuth-/OIDC- oder
Entitlement-Infrastruktur eingeführt. Der Token wird ausschließlich über die
geschützte App-Konfiguration bereitgestellt und mit konstanter Zeit verglichen.
Die Implementierung verwendet dafür den Konfigurationswert
`MARKET_MCP_TOKEN`.
Die fachliche Ownership der Lizenz-/Entitlement-Daten bleibt offen. Die
Schnittstelle prüft nur die über den bestehenden Zugriffskontext gelieferte
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/service.php';
function market_management_mcp_token(array $env): string
{
$token = env_value('MARKET_MCP_TOKEN', $env);
if ($token === '') {
throw new RuntimeException('MARKET_MCP_TOKEN ist nicht konfiguriert.');
}
return $token;
}
function market_management_mcp_authorize(array $env): string
{
$header = (string) ($_SERVER['HTTP_AUTHORIZATION'] ?? '');
if (!preg_match('/^Bearer\s+(.+)$/i', trim($header), $matches)) {
throw new RuntimeException('Bearer-Token fehlt.');
}
$token = trim($matches[1]);
$expected = market_management_mcp_token($env);
if ($token === '' || !hash_equals($expected, $token)) {
throw new RuntimeException('Bearer-Token ist ungültig.');
}
return 'bearer:' . hash('sha256', $token);
}
function market_management_mcp_tools(): array
{
return [
['name' => 'market_management.search_markets', 'description' => 'Märkte suchen', 'inputSchema' => ['type' => 'object']],
['name' => 'market_management.get_market', 'description' => 'Markt mit Durchführungen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
['name' => 'market_management.create_market', 'description' => 'Markt mit bestehender Kategorie anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['name', 'category_id']]],
['name' => 'market_management.update_market', 'description' => 'Markt ändern', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
['name' => 'market_management.archive_market', 'description' => 'Markt archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
['name' => 'market_management.reactivate_market', 'description' => 'Markt reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['market_id']]],
['name' => 'market_management.list_categories', 'description' => 'Bestehende Kategorien lesen', 'inputSchema' => ['type' => 'object']],
['name' => 'market_management.create_occurrence', 'description' => 'Durchführung anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['market_definition_id']]],
['name' => 'market_management.get_occurrence', 'description' => 'Durchführung mit Tagen lesen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
['name' => 'market_management.update_occurrence', 'description' => 'Durchführung ändern', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
['name' => 'market_management.archive_occurrence', 'description' => 'Durchführung archivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
['name' => 'market_management.reactivate_occurrence', 'description' => 'Durchführung reaktivieren', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id']]],
['name' => 'market_management.create_day', 'description' => 'Durchführungstag anlegen', 'inputSchema' => ['type' => 'object', 'required' => ['occurrence_id', 'date']]],
['name' => 'market_management.update_day', 'description' => 'Durchführungstag ändern', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]],
['name' => 'market_management.delete_day', 'description' => 'Durchführungstag entfernen', 'inputSchema' => ['type' => 'object', 'required' => ['day_id']]],
];
}
function market_management_mcp_call(PDO $pdo, string $tool, array $arguments): array
{
return match ($tool) {
'market_management.search_markets' => market_management_search_markets($pdo, $arguments),
'market_management.get_market' => market_management_get_market(
$pdo,
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
(bool) ($arguments['include_archived'] ?? false)
) ?? throw new RuntimeException('Markt nicht gefunden.'),
'market_management.create_market' => market_management_create_market($pdo, $arguments),
'market_management.update_market' => market_management_update_market(
$pdo,
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
$arguments
),
'market_management.archive_market' => market_management_set_market_archive(
$pdo,
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
true
),
'market_management.reactivate_market' => market_management_set_market_archive(
$pdo,
market_management_require_positive_id($arguments['market_id'] ?? null, 'market_id'),
false
),
'market_management.list_categories' => market_management_list_categories($pdo),
'market_management.create_occurrence' => market_management_create_occurrence($pdo, $arguments),
'market_management.get_occurrence' => market_management_get_occurrence(
$pdo,
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
(bool) ($arguments['include_archived'] ?? false)
) ?? throw new RuntimeException('Durchführung nicht gefunden.'),
'market_management.update_occurrence' => market_management_update_occurrence(
$pdo,
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
$arguments
),
'market_management.archive_occurrence' => market_management_set_occurrence_archive(
$pdo,
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
true
),
'market_management.reactivate_occurrence' => market_management_set_occurrence_archive(
$pdo,
market_management_require_positive_id($arguments['occurrence_id'] ?? null, 'occurrence_id'),
false
),
'market_management.create_day' => market_management_create_day($pdo, $arguments),
'market_management.update_day' => market_management_update_day(
$pdo,
market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id'),
$arguments
),
'market_management.delete_day' => market_management_delete_day(
$pdo,
market_management_require_positive_id($arguments['day_id'] ?? null, 'day_id')
),
default => throw new InvalidArgumentException('Unbekanntes MCP-Tool: ' . $tool),
};
}
+444
View File
@@ -0,0 +1,444 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../shared/db.php';
const MARKET_MANAGEMENT_STATUSES = [
'open',
'waitlist',
'registered',
'confirmation_pending',
'confirmed',
'cancelled',
'completed',
];
function market_management_require_positive_id(mixed $value, string $field): int
{
if (!is_int($value) && !ctype_digit((string) $value)) {
throw new InvalidArgumentException($field . ' muss eine positive Ganzzahl sein.');
}
$id = (int) $value;
if ($id <= 0) {
throw new InvalidArgumentException($field . ' muss eine positive Ganzzahl sein.');
}
return $id;
}
function market_management_optional_string(array $input, string $field): ?string
{
if (!array_key_exists($field, $input) || $input[$field] === null) {
return null;
}
$value = trim((string) $input[$field]);
return $value === '' ? null : $value;
}
function market_management_optional_bool(array $input, string $field): ?bool
{
if (!array_key_exists($field, $input) || $input[$field] === null) {
return null;
}
if (is_bool($input[$field])) {
return $input[$field];
}
if ($input[$field] === 'true' || $input[$field] === '1' || $input[$field] === 1) {
return true;
}
if ($input[$field] === 'false' || $input[$field] === '0' || $input[$field] === 0) {
return false;
}
throw new InvalidArgumentException($field . ' muss boolean sein.');
}
function market_management_optional_number(array $input, string $field): ?string
{
if (!array_key_exists($field, $input) || $input[$field] === null || $input[$field] === '') {
return null;
}
$value = parse_number($input[$field]);
if ($value === null || $value < 0) {
throw new InvalidArgumentException($field . ' muss eine nichtnegative Zahl sein.');
}
return number_format($value, 2, '.', '');
}
function market_management_optional_integer(array $input, string $field): ?int
{
if (!array_key_exists($field, $input) || $input[$field] === null || $input[$field] === '') {
return null;
}
$value = parse_number($input[$field]);
if ($value === null || $value < 0 || floor($value) !== $value) {
throw new InvalidArgumentException($field . ' muss eine nichtnegative Ganzzahl sein.');
}
return (int) $value;
}
function market_management_category_exists(PDO $pdo, int $categoryId): bool
{
$stmt = $pdo->prepare('SELECT 1 FROM market_category WHERE id = :id AND is_active = TRUE');
$stmt->execute([':id' => $categoryId]);
return $stmt->fetchColumn() !== false;
}
function market_management_create_market(PDO $pdo, array $input): array
{
$name = trim((string) ($input['name'] ?? ''));
$categoryId = market_management_require_positive_id($input['category_id'] ?? null, 'category_id');
if ($name === '') {
throw new InvalidArgumentException('name ist erforderlich.');
}
if (!market_management_category_exists($pdo, $categoryId)) {
throw new InvalidArgumentException('category_id verweist nicht auf eine aktive Kategorie.');
}
$stmt = $pdo->prepare(
'INSERT INTO market_definition (name, category_id)
VALUES (:name, :category_id)
RETURNING id, name, category_id, archived_at, created_at, updated_at'
);
$stmt->execute([':name' => $name, ':category_id' => $categoryId]);
return (array) $stmt->fetch();
}
function market_management_search_markets(PDO $pdo, array $input): array
{
$search = market_management_optional_string($input, 'search');
$includeArchived = (bool) ($input['include_archived'] ?? false);
$limit = min(100, max(1, (int) ($input['limit'] ?? 50)));
$where = [];
$params = [':limit' => $limit];
if (!$includeArchived) {
$where[] = 'm.archived_at IS NULL';
}
if ($search !== null) {
$where[] = 'm.name ILIKE :search';
$params[':search'] = '%' . $search . '%';
}
$stmt = $pdo->prepare(
'SELECT m.id, m.name, m.category_id, c.code AS category_code, c.name AS category_name,
m.archived_at, m.created_at, m.updated_at
FROM market_definition m
INNER JOIN market_category c ON c.id = m.category_id
WHERE ' . ($where === [] ? 'TRUE' : implode(' AND ', $where)) . '
ORDER BY m.name ASC, m.id ASC
LIMIT :limit'
);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value, $key === ':limit' ? PDO::PARAM_INT : PDO::PARAM_STR);
}
$stmt->execute();
return $stmt->fetchAll();
}
function market_management_get_market(PDO $pdo, int $marketId, bool $includeArchived = false): ?array
{
$stmt = $pdo->prepare(
'SELECT m.id, m.name, m.category_id, c.code AS category_code, c.name AS category_name,
m.archived_at, m.created_at, m.updated_at
FROM market_definition m
INNER JOIN market_category c ON c.id = m.category_id
WHERE m.id = :id AND (:include_archived OR m.archived_at IS NULL)'
);
$stmt->execute([':id' => $marketId, ':include_archived' => $includeArchived]);
$market = $stmt->fetch();
if (!is_array($market)) {
return null;
}
$occurrences = $pdo->prepare(
'SELECT id, status, place, address, municipality_reference_id, notes,
registration_deadline, request_or_registration_date, confirmation_date,
payment_deadline, payment_status, payment_date, paid_amount,
contract_folder_url, archived_at, created_at, updated_at
FROM market_occurrence
WHERE market_definition_id = :market_id
AND (:include_archived OR archived_at IS NULL)
ORDER BY id ASC'
);
$occurrences->execute([':market_id' => $marketId, ':include_archived' => $includeArchived]);
$market['occurrences'] = $occurrences->fetchAll();
return $market;
}
function market_management_update_market(PDO $pdo, int $marketId, array $input): array
{
$sets = [];
$params = [':id' => $marketId];
if (array_key_exists('name', $input)) {
$name = trim((string) $input['name']);
if ($name === '') {
throw new InvalidArgumentException('name darf nicht leer sein.');
}
$sets[] = 'name = :name';
$params[':name'] = $name;
}
if (array_key_exists('category_id', $input)) {
$categoryId = market_management_require_positive_id($input['category_id'], 'category_id');
if (!market_management_category_exists($pdo, $categoryId)) {
throw new InvalidArgumentException('category_id verweist nicht auf eine aktive Kategorie.');
}
$sets[] = 'category_id = :category_id';
$params[':category_id'] = $categoryId;
}
if ($sets === []) {
throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.');
}
$sets[] = 'updated_at = NOW()';
$stmt = $pdo->prepare(
'UPDATE market_definition SET ' . implode(', ', $sets) . ' WHERE id = :id
RETURNING id, name, category_id, archived_at, created_at, updated_at'
);
$stmt->execute($params);
$market = $stmt->fetch();
if (!is_array($market)) {
throw new RuntimeException('Markt nicht gefunden.');
}
return $market;
}
function market_management_set_market_archive(PDO $pdo, int $marketId, bool $archive): array
{
$stmt = $pdo->prepare(
'UPDATE market_definition
SET archived_at = ' . ($archive ? 'COALESCE(archived_at, NOW())' : 'NULL') . ', updated_at = NOW()
WHERE id = :id
RETURNING id, name, category_id, archived_at, created_at, updated_at'
);
$stmt->execute([':id' => $marketId]);
$market = $stmt->fetch();
if (!is_array($market)) {
throw new RuntimeException('Markt nicht gefunden.');
}
return $market;
}
function market_management_list_categories(PDO $pdo): array
{
return $pdo->query(
'SELECT id, code, name FROM market_category WHERE is_active = TRUE ORDER BY name ASC, id ASC'
)->fetchAll();
}
function market_management_validate_status(mixed $status): string
{
$value = trim((string) $status);
if (!in_array($value, MARKET_MANAGEMENT_STATUSES, true)) {
throw new InvalidArgumentException('Ungültiger Durchführungsstatus.');
}
return $value;
}
function market_management_write_fields(array $input, array $definitions): array
{
$fields = [];
$params = [];
foreach ($definitions as $field => $type) {
if (!array_key_exists($field, $input)) {
continue;
}
$value = match ($type) {
'string' => market_management_optional_string($input, $field),
'bool' => market_management_optional_bool($input, $field),
'number' => market_management_optional_number($input, $field),
'integer' => market_management_optional_integer($input, $field),
'id' => market_management_require_positive_id($input[$field], $field),
default => throw new LogicException('Unbekannter Feldtyp.'),
};
$fields[] = $field . ' = :' . $field;
$params[':' . $field] = $value;
}
return [$fields, $params];
}
function market_management_create_occurrence(PDO $pdo, array $input): array
{
$marketId = market_management_require_positive_id($input['market_definition_id'] ?? null, 'market_definition_id');
if (market_management_get_market($pdo, $marketId, true) === null) {
throw new InvalidArgumentException('market_definition_id verweist nicht auf einen Markt.');
}
$status = array_key_exists('status', $input) ? market_management_validate_status($input['status']) : 'open';
$definitions = [
'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'id', 'notes' => 'string',
'registration_deadline' => 'string', 'request_or_registration_date' => 'string', 'confirmation_date' => 'string',
'payment_deadline' => 'string', 'stand_size' => 'string', 'table_available' => 'bool',
'table_rentable' => 'bool', 'own_table_required' => 'bool', 'shelter_available' => 'bool',
'power_available' => 'bool', 'water_available' => 'bool', 'lighting_available' => 'bool',
'other_infrastructure' => 'string', 'own_stand_requirements' => 'string', 'travel_address' => 'string',
'travel_distance_km' => 'number', 'travel_duration_minutes' => 'integer', 'parking_options' => 'string',
'delivery_access' => 'string', 'direct_unload_possible' => 'bool', 'storage_options' => 'string',
'travel_notes' => 'string', 'payment_status' => 'string', 'payment_date' => 'string',
'paid_amount' => 'number', 'payment_method_code' => 'string', 'cancellation_terms' => 'string',
'revenue_share_terms' => 'string', 'other_participation_conditions' => 'string', 'contract_folder_url' => 'string',
];
[$fields, $params] = market_management_write_fields($input, $definitions);
if (isset($params[':payment_status']) && !in_array($params[':payment_status'], ['paid', 'unpaid'], true)) {
throw new InvalidArgumentException('Ungültiger Zahlungsstatus.');
}
array_unshift($fields, 'market_definition_id = :market_definition_id', 'status = :status');
$params[':market_definition_id'] = $marketId;
$params[':status'] = $status;
$columns = array_map(static fn (string $field): string => explode(' = ', $field, 2)[0], $fields);
$values = array_map(static fn (string $field): string => explode(' = ', $field, 2)[1], $fields);
$stmt = $pdo->prepare(
'INSERT INTO market_occurrence (' . implode(', ', $columns) . ')
VALUES (' . implode(', ', $values) . ')
RETURNING *'
);
$stmt->execute($params);
return (array) $stmt->fetch();
}
function market_management_get_occurrence(PDO $pdo, int $occurrenceId, bool $includeArchived = false): ?array
{
$stmt = $pdo->prepare(
'SELECT * FROM market_occurrence
WHERE id = :id AND (:include_archived OR archived_at IS NULL)'
);
$stmt->execute([':id' => $occurrenceId, ':include_archived' => $includeArchived]);
$occurrence = $stmt->fetch();
if (!is_array($occurrence)) {
return null;
}
$days = $pdo->prepare(
'SELECT * FROM market_occurrence_day
WHERE occurrence_id = :occurrence_id ORDER BY date ASC, id ASC'
);
$days->execute([':occurrence_id' => $occurrenceId]);
$occurrence['days'] = $days->fetchAll();
return $occurrence;
}
function market_management_update_occurrence(PDO $pdo, int $occurrenceId, array $input): array
{
$definitions = [
'status' => 'string', 'place' => 'string', 'address' => 'string', 'municipality_reference_id' => 'id',
'notes' => 'string', 'registration_deadline' => 'string', 'request_or_registration_date' => 'string',
'confirmation_date' => 'string', 'payment_deadline' => 'string', 'stand_size' => 'string',
'table_available' => 'bool', 'table_rentable' => 'bool', 'own_table_required' => 'bool',
'shelter_available' => 'bool', 'power_available' => 'bool', 'water_available' => 'bool',
'lighting_available' => 'bool', 'other_infrastructure' => 'string', 'own_stand_requirements' => 'string',
'travel_address' => 'string', 'travel_distance_km' => 'number', 'travel_duration_minutes' => 'integer',
'parking_options' => 'string', 'delivery_access' => 'string', 'direct_unload_possible' => 'bool',
'storage_options' => 'string', 'travel_notes' => 'string', 'payment_status' => 'string',
'payment_date' => 'string', 'paid_amount' => 'number', 'payment_method_code' => 'string',
'cancellation_terms' => 'string', 'revenue_share_terms' => 'string',
'other_participation_conditions' => 'string', 'contract_folder_url' => 'string',
];
[$fields, $params] = market_management_write_fields($input, $definitions);
if (array_key_exists('status', $input)) {
$params[':status'] = market_management_validate_status($input['status']);
}
if (isset($params[':payment_status']) && !in_array($params[':payment_status'], ['paid', 'unpaid'], true)) {
throw new InvalidArgumentException('Ungültiger Zahlungsstatus.');
}
if ($fields === []) {
throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.');
}
$fields[] = 'updated_at = NOW()';
$params[':id'] = $occurrenceId;
$stmt = $pdo->prepare(
'UPDATE market_occurrence SET ' . implode(', ', $fields) . ' WHERE id = :id RETURNING *'
);
$stmt->execute($params);
$occurrence = $stmt->fetch();
if (!is_array($occurrence)) {
throw new RuntimeException('Durchführung nicht gefunden.');
}
return $occurrence;
}
function market_management_create_day(PDO $pdo, array $input): array
{
$occurrenceId = market_management_require_positive_id($input['occurrence_id'] ?? null, 'occurrence_id');
if (market_management_get_occurrence($pdo, $occurrenceId, true) === null) {
throw new InvalidArgumentException('occurrence_id verweist nicht auf eine Durchführung.');
}
$date = trim((string) ($input['date'] ?? ''));
if ($date === '') {
throw new InvalidArgumentException('date ist erforderlich.');
}
$definitions = [
'stand_start_time' => 'string', 'stand_end_time' => 'string', 'setup_start_time' => 'string',
'setup_end_time' => 'string', 'delivery_start_time' => 'string', 'delivery_end_time' => 'string',
'observations' => 'string',
];
[$fields, $params] = market_management_write_fields($input, $definitions);
array_unshift($fields, 'occurrence_id = :occurrence_id', 'date = :date');
$params[':occurrence_id'] = $occurrenceId;
$params[':date'] = $date;
$columns = array_map(static fn (string $field): string => explode(' = ', $field, 2)[0], $fields);
$values = array_map(static fn (string $field): string => explode(' = ', $field, 2)[1], $fields);
$stmt = $pdo->prepare(
'INSERT INTO market_occurrence_day (' . implode(', ', $columns) . ')
VALUES (' . implode(', ', $values) . ') RETURNING *'
);
$stmt->execute($params);
return (array) $stmt->fetch();
}
function market_management_update_day(PDO $pdo, int $dayId, array $input): array
{
$definitions = [
'date' => 'string', 'stand_start_time' => 'string', 'stand_end_time' => 'string',
'setup_start_time' => 'string', 'setup_end_time' => 'string', 'delivery_start_time' => 'string',
'delivery_end_time' => 'string', 'observations' => 'string',
];
[$fields, $params] = market_management_write_fields($input, $definitions);
if ($fields === []) {
throw new InvalidArgumentException('Mindestens ein änderbares Feld ist erforderlich.');
}
$fields[] = 'updated_at = NOW()';
$params[':id'] = $dayId;
$stmt = $pdo->prepare(
'UPDATE market_occurrence_day SET ' . implode(', ', $fields) . ' WHERE id = :id RETURNING *'
);
$stmt->execute($params);
$day = $stmt->fetch();
if (!is_array($day)) {
throw new RuntimeException('Durchführungstag nicht gefunden.');
}
return $day;
}
function market_management_set_occurrence_archive(PDO $pdo, int $occurrenceId, bool $archive): array
{
$stmt = $pdo->prepare(
'UPDATE market_occurrence
SET archived_at = ' . ($archive ? 'COALESCE(archived_at, NOW())' : 'NULL') . ', updated_at = NOW()
WHERE id = :id RETURNING *'
);
$stmt->execute([':id' => $occurrenceId]);
$occurrence = $stmt->fetch();
if (!is_array($occurrence)) {
throw new RuntimeException('Durchführung nicht gefunden.');
}
return $occurrence;
}
function market_management_delete_day(PDO $pdo, int $dayId): array
{
$stmt = $pdo->prepare('DELETE FROM market_occurrence_day WHERE id = :id RETURNING id, occurrence_id, date');
$stmt->execute([':id' => $dayId]);
$day = $stmt->fetch();
if (!is_array($day)) {
throw new RuntimeException('Durchführungstag nicht gefunden.');
}
return $day;
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../modules/shared/db.php';
require_once __DIR__ . '/../../modules/market_management/mcp.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$env = expand_env_values(parse_env_file(__DIR__ . '/../../.env'));
try {
$userSubject = market_management_mcp_authorize($env);
$pdo = connect_database($env);
$body = file_get_contents('php://input');
$request = json_decode($body === false ? '' : $body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($request) || ($request['jsonrpc'] ?? '') !== '2.0') {
throw new InvalidArgumentException('Ungültige JSON-RPC-Anfrage.');
}
$id = $request['id'] ?? null;
$method = (string) ($request['method'] ?? '');
$params = is_array($request['params'] ?? null) ? $request['params'] : [];
if ($method === 'initialize') {
$result = [
'protocolVersion' => (string) ($params['protocolVersion'] ?? '2025-03-26'),
'capabilities' => ['tools' => ['listChanged' => false]],
'serverInfo' => ['name' => 'naurua-market-management', 'version' => '1.0.0'],
];
} elseif ($method === 'notifications/initialized') {
http_response_code(202);
exit;
} elseif ($method === 'tools/list') {
$result = ['tools' => market_management_mcp_tools()];
} elseif ($method === 'tools/call') {
$tool = (string) ($params['name'] ?? '');
$arguments = is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
$result = [
'content' => [[
'type' => 'text',
'text' => json_encode(market_management_mcp_call($pdo, $tool, $arguments), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
]],
'isError' => false,
];
} else {
throw new InvalidArgumentException('Unbekannte JSON-RPC-Methode: ' . $method);
}
echo json_encode(['jsonrpc' => '2.0', 'id' => $id, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
} catch (Throwable $e) {
http_response_code($e instanceof RuntimeException && str_contains($e->getMessage(), 'Token') ? 401 : 400);
echo json_encode([
'jsonrpc' => '2.0',
'id' => $request['id'] ?? null,
'error' => ['code' => -32600, 'message' => $e->getMessage()],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}