create-prisma-php-app 4.0.0-alpha.19 → 4.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.htaccess +54 -41
- package/dist/bootstrap.php +133 -88
- package/dist/index.js +167 -72
- package/dist/settings/auto-swagger-docs.ts +196 -95
- package/dist/settings/bs-config.ts +56 -58
- package/dist/settings/restart-mcp.ts +58 -0
- package/dist/settings/restart-websocket.ts +44 -45
- package/dist/settings/utils.ts +240 -0
- package/dist/src/Lib/Headers/Boom.php +152 -168
- package/dist/src/Lib/IncludeTracker.php +1 -1
- package/dist/src/Lib/MCP/WeatherTools.php +104 -0
- package/dist/src/Lib/MCP/mcp-server.php +80 -0
- package/dist/src/Lib/MainLayout.php +19 -4
- package/dist/src/Lib/Middleware/CorsMiddleware.php +158 -0
- package/dist/src/Lib/PHPX/Fragment.php +32 -0
- package/dist/src/Lib/PHPX/PHPX.php +119 -5
- package/dist/src/Lib/PHPX/TemplateCompiler.php +78 -21
- package/dist/src/Lib/PHPX/TwMerge.php +305 -154
- package/dist/src/Lib/PHPX/TypeCoercer.php +5 -5
- package/dist/src/Lib/Request.php +4 -1
- package/dist/src/Lib/Security/RateLimiter.php +33 -0
- package/dist/src/Lib/Validator.php +20 -6
- package/dist/src/Lib/Websocket/websocket-server.php +105 -14
- package/dist/src/app/index.php +24 -5
- package/dist/src/app/js/index.js +1 -1
- package/package.json +1 -1
- package/dist/settings/restart-websocket.bat +0 -28
- package/dist/src/app/assets/images/prisma-php-black.svg +0 -6
|
@@ -4,203 +4,187 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Lib\Headers;
|
|
6
6
|
|
|
7
|
+
use InvalidArgumentException;
|
|
8
|
+
use JsonException;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* HTTP‑error helper.
|
|
12
|
+
*
|
|
13
|
+
* @method static self badRequest(string $message = 'Bad Request', array $details = [])
|
|
14
|
+
* @method static self unauthorized(string $message = 'Unauthorized', array $details = [])
|
|
15
|
+
* @method static self paymentRequired(string $message = 'Payment Required', array $details = [])
|
|
16
|
+
* @method static self forbidden(string $message = 'Forbidden', array $details = [])
|
|
17
|
+
* @method static self notFound(string $message = 'Not Found', array $details = [])
|
|
18
|
+
* @method static self methodNotAllowed(string $message = 'Method Not Allowed', array $details = [])
|
|
19
|
+
* @method static self notAcceptable(string $message = 'Not Acceptable', array $details = [])
|
|
20
|
+
* @method static self conflict(string $message = 'Conflict', array $details = [])
|
|
21
|
+
* @method static self gone(string $message = 'Gone', array $details = [])
|
|
22
|
+
* @method static self lengthRequired(string $message = 'Length Required', array $details = [])
|
|
23
|
+
* @method static self preconditionFailed(string $message = 'Precondition Failed', array $details = [])
|
|
24
|
+
* @method static self payloadTooLarge(string $message = 'Payload Too Large', array $details = [])
|
|
25
|
+
* @method static self uriTooLarge(string $message = 'URI Too Large', array $details = [])
|
|
26
|
+
* @method static self unsupportedMediaType(string $message = 'Unsupported Media Type', array $details = [])
|
|
27
|
+
* @method static self rangeNotSatisfiable(string $message = 'Range Not Satisfiable', array $details = [])
|
|
28
|
+
* @method static self expectationFailed(string $message = 'Expectation Failed', array $details = [])
|
|
29
|
+
* @method static self iAmATeapot(string $message = "I'm a teapot", array $details = [])
|
|
30
|
+
* @method static self misdirectedRequest(string $message = 'Misdirected Request', array $details = [])
|
|
31
|
+
* @method static self unprocessableEntity(string $message = 'Unprocessable Entity', array $details = [])
|
|
32
|
+
* @method static self locked(string $message = 'Locked', array $details = [])
|
|
33
|
+
* @method static self failedDependency(string $message = 'Failed Dependency', array $details = [])
|
|
34
|
+
* @method static self tooEarly(string $message = 'Too Early', array $details = [])
|
|
35
|
+
* @method static self upgradeRequired(string $message = 'Upgrade Required', array $details = [])
|
|
36
|
+
* @method static self preconditionRequired(string $message = 'Precondition Required', array $details = [])
|
|
37
|
+
* @method static self tooManyRequests(string $message = 'Too Many Requests', array $details = [])
|
|
38
|
+
* @method static self requestHeaderFieldsTooLarge(string $message = 'Request Header Fields Too Large', array $details = [])
|
|
39
|
+
* @method static self unavailableForLegalReasons(string $message = 'Unavailable for Legal Reasons', array $details = [])
|
|
40
|
+
* @method static self internal(string $message = 'Internal Server Error', array $details = [])
|
|
41
|
+
* @method static self notImplemented(string $message = 'Not Implemented', array $details = [])
|
|
42
|
+
* @method static self badGateway(string $message = 'Bad Gateway', array $details = [])
|
|
43
|
+
* @method static self serviceUnavailable(string $message = 'Service Unavailable', array $details = [])
|
|
44
|
+
* @method static self gatewayTimeout(string $message = 'Gateway Timeout', array $details = [])
|
|
45
|
+
* @method static self httpVersionNotSupported(string $message = 'HTTP Version Not Supported', array $details = [])
|
|
46
|
+
* @method static self insufficientStorage(string $message = 'Insufficient Storage', array $details = [])
|
|
47
|
+
* @method static self loopDetected(string $message = 'Loop Detected', array $details = [])
|
|
48
|
+
* @method static self notExtended(string $message = 'Not Extended', array $details = [])
|
|
49
|
+
* @method static self networkAuthenticationRequired(string $message = 'Network Authentication Required', array $details = [])
|
|
50
|
+
* @method static self networkConnectTimeoutError(string $message = 'Network Connect Timeout Error', array $details = [])
|
|
51
|
+
*/
|
|
7
52
|
class Boom
|
|
8
53
|
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
private const PHRASES = [
|
|
55
|
+
/* 4XX Client error */
|
|
56
|
+
400 => 'Bad Request',
|
|
57
|
+
401 => 'Unauthorized',
|
|
58
|
+
402 => 'Payment Required',
|
|
59
|
+
403 => 'Forbidden',
|
|
60
|
+
404 => 'Not Found',
|
|
61
|
+
405 => 'Method Not Allowed',
|
|
62
|
+
406 => 'Not Acceptable',
|
|
63
|
+
407 => 'Proxy Authentication Required',
|
|
64
|
+
408 => 'Request Timeout',
|
|
65
|
+
409 => 'Conflict',
|
|
66
|
+
410 => 'Gone',
|
|
67
|
+
411 => 'Length Required',
|
|
68
|
+
412 => 'Precondition Failed',
|
|
69
|
+
413 => 'Payload Too Large',
|
|
70
|
+
414 => 'URI Too Large',
|
|
71
|
+
415 => 'Unsupported Media Type',
|
|
72
|
+
416 => 'Range Not Satisfiable',
|
|
73
|
+
417 => 'Expectation Failed',
|
|
74
|
+
418 => "I'm a teapot",
|
|
75
|
+
421 => 'Misdirected Request',
|
|
76
|
+
422 => 'Unprocessable Entity',
|
|
77
|
+
423 => 'Locked',
|
|
78
|
+
424 => 'Failed Dependency',
|
|
79
|
+
425 => 'Too Early',
|
|
80
|
+
426 => 'Upgrade Required',
|
|
81
|
+
428 => 'Precondition Required',
|
|
82
|
+
429 => 'Too Many Requests',
|
|
83
|
+
431 => 'Request Header Fields Too Large',
|
|
84
|
+
451 => 'Unavailable for Legal Reasons',
|
|
85
|
+
499 => 'Client Closed Request',
|
|
86
|
+
|
|
87
|
+
/* 5XX Server error */
|
|
88
|
+
500 => 'Internal Server Error',
|
|
89
|
+
501 => 'Not Implemented',
|
|
90
|
+
502 => 'Bad Gateway',
|
|
91
|
+
503 => 'Service Unavailable',
|
|
92
|
+
504 => 'Gateway Timeout',
|
|
93
|
+
505 => 'HTTP Version Not Supported',
|
|
94
|
+
507 => 'Insufficient Storage',
|
|
95
|
+
508 => 'Loop Detected',
|
|
96
|
+
510 => 'Not Extended',
|
|
97
|
+
511 => 'Network Authentication Required',
|
|
98
|
+
599 => 'Network Connect Timeout Error',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/** @var int */
|
|
102
|
+
protected int $statusCode;
|
|
103
|
+
|
|
104
|
+
/** @var string */
|
|
105
|
+
protected string $errorMessage;
|
|
106
|
+
|
|
107
|
+
/** @var array<string,mixed> */
|
|
108
|
+
protected array $errorDetails;
|
|
109
|
+
|
|
110
|
+
public function __construct(
|
|
111
|
+
int $statusCode,
|
|
112
|
+
string $errorMessage = '',
|
|
113
|
+
array $errorDetails = [],
|
|
114
|
+
) {
|
|
115
|
+
if (!isset(self::PHRASES[$statusCode])) {
|
|
116
|
+
throw new InvalidArgumentException("Unsupported HTTP status code: $statusCode");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
$this->statusCode = $statusCode;
|
|
120
|
+
$this->errorMessage = $errorMessage ?: self::PHRASES[$statusCode];
|
|
121
|
+
$this->errorDetails = $errorDetails;
|
|
55
122
|
}
|
|
56
123
|
|
|
57
|
-
|
|
58
|
-
* Factory method for 401 Unauthorized.
|
|
59
|
-
*
|
|
60
|
-
* @param string $message Error message.
|
|
61
|
-
* @param array $details Additional error details.
|
|
62
|
-
*
|
|
63
|
-
* @return self
|
|
64
|
-
*/
|
|
65
|
-
public static function unauthorized(string $message = 'Unauthorized', array $details = []): self
|
|
124
|
+
public static function create(int $code, ?string $msg = null, array $details = []): self
|
|
66
125
|
{
|
|
67
|
-
return new self(
|
|
126
|
+
return new self($code, $msg ?? '', $details);
|
|
68
127
|
}
|
|
69
128
|
|
|
70
129
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @param string $message Error message.
|
|
74
|
-
* @param array $details Additional error details.
|
|
75
|
-
*
|
|
76
|
-
* @return self
|
|
77
|
-
*/
|
|
78
|
-
public static function paymentRequired(string $message = 'Payment Required', array $details = []): self
|
|
79
|
-
{
|
|
80
|
-
return new self(402, $message, $details);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Factory method for 403 Forbidden.
|
|
85
|
-
*
|
|
86
|
-
* @param string $message Error message.
|
|
87
|
-
* @param array $details Additional error details.
|
|
88
|
-
*
|
|
89
|
-
* @return self
|
|
90
|
-
*/
|
|
91
|
-
public static function forbidden(string $message = 'Forbidden', array $details = []): self
|
|
92
|
-
{
|
|
93
|
-
return new self(403, $message, $details);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Factory method for 404 Not Found.
|
|
98
|
-
*
|
|
99
|
-
* @param string $message Error message.
|
|
100
|
-
* @param array $details Additional error details.
|
|
130
|
+
* Dynamic factories: Boom::tooManyRequests(), Boom::badRequest(), …
|
|
101
131
|
*
|
|
102
|
-
* @
|
|
132
|
+
* @param array{0?:string,1?:array<mixed>} $args
|
|
103
133
|
*/
|
|
104
|
-
public static function
|
|
134
|
+
public static function __callStatic(string $method, array $args): self
|
|
105
135
|
{
|
|
106
|
-
|
|
136
|
+
// Convert camelCase to Studly Caps → Reason‑Phrase → code
|
|
137
|
+
$normalized = strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', $method) ?? '');
|
|
138
|
+
$code = array_search(
|
|
139
|
+
ucwords(str_replace(' ', ' ', $normalized)),
|
|
140
|
+
self::PHRASES,
|
|
141
|
+
true
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if ($code === false) {
|
|
145
|
+
throw new InvalidArgumentException("Undefined Boom factory: $method()");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$msg = $args[0] ?? '';
|
|
149
|
+
$details = $args[1] ?? [];
|
|
150
|
+
|
|
151
|
+
return new self((int)$code, $msg, $details);
|
|
107
152
|
}
|
|
108
153
|
|
|
109
|
-
/**
|
|
110
|
-
* Factory method for 405 Method Not Allowed.
|
|
111
|
-
*
|
|
112
|
-
* @param string $message Error message.
|
|
113
|
-
* @param array $details Additional error details.
|
|
114
|
-
*
|
|
115
|
-
* @return self
|
|
116
|
-
*/
|
|
117
|
-
public static function methodNotAllowed(string $message = 'Method Not Allowed', array $details = []): self
|
|
118
|
-
{
|
|
119
|
-
return new self(405, $message, $details);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Factory method for 406 Not Acceptable.
|
|
124
|
-
*
|
|
125
|
-
* @param string $message Error message.
|
|
126
|
-
* @param array $details Additional error details.
|
|
127
|
-
*
|
|
128
|
-
* @return self
|
|
129
|
-
*/
|
|
130
|
-
public static function notAcceptable(string $message = 'Not Acceptable', array $details = []): self
|
|
131
|
-
{
|
|
132
|
-
return new self(406, $message, $details);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Factory method for 500 Internal Server Error.
|
|
137
|
-
*
|
|
138
|
-
* @param string $message Error message.
|
|
139
|
-
* @param array $details Additional error details.
|
|
140
|
-
*
|
|
141
|
-
* @return self
|
|
142
|
-
*/
|
|
143
|
-
public static function internal(string $message = 'Internal Server Error', array $details = []): self
|
|
144
|
-
{
|
|
145
|
-
return new self(500, $message, $details);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Sends the HTTP error response and terminates the script.
|
|
150
|
-
*
|
|
151
|
-
* @return void
|
|
152
|
-
*/
|
|
153
154
|
public function toResponse(): void
|
|
154
155
|
{
|
|
155
156
|
http_response_code($this->statusCode);
|
|
156
|
-
header('Content-Type: application/json');
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
header('Content-Type: application/json; charset=utf-8');
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
echo json_encode(
|
|
161
|
+
[
|
|
162
|
+
'statusCode' => $this->statusCode,
|
|
163
|
+
'error' => $this->errorMessage,
|
|
164
|
+
'details' => (object)$this->errorDetails,
|
|
165
|
+
],
|
|
166
|
+
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
|
|
167
|
+
);
|
|
168
|
+
} catch (JsonException $e) {
|
|
169
|
+
echo '{"statusCode":500,"error":"JSON encoding error"}';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
exit; // Ensure no further output
|
|
165
173
|
}
|
|
166
174
|
|
|
167
|
-
|
|
168
|
-
* Checks if the provided error is an instance of Boom.
|
|
169
|
-
*
|
|
170
|
-
* @param mixed $error The error to check.
|
|
171
|
-
*
|
|
172
|
-
* @return bool
|
|
173
|
-
*/
|
|
174
|
-
public static function isBoom($error): bool
|
|
175
|
+
public static function isBoom(mixed $err): bool
|
|
175
176
|
{
|
|
176
|
-
return $
|
|
177
|
+
return $err instanceof self;
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
/**
|
|
180
|
-
* Gets the HTTP status code.
|
|
181
|
-
*
|
|
182
|
-
* @return int
|
|
183
|
-
*/
|
|
184
180
|
public function getStatusCode(): int
|
|
185
181
|
{
|
|
186
182
|
return $this->statusCode;
|
|
187
183
|
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Gets the error message.
|
|
191
|
-
*
|
|
192
|
-
* @return string
|
|
193
|
-
*/
|
|
194
184
|
public function getErrorMessage(): string
|
|
195
185
|
{
|
|
196
186
|
return $this->errorMessage;
|
|
197
187
|
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Gets the additional error details.
|
|
201
|
-
*
|
|
202
|
-
* @return array
|
|
203
|
-
*/
|
|
204
188
|
public function getErrorDetails(): array
|
|
205
189
|
{
|
|
206
190
|
return $this->errorDetails;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\MCP;
|
|
6
|
+
|
|
7
|
+
use GuzzleHttp\Client;
|
|
8
|
+
use GuzzleHttp\Exception\GuzzleException;
|
|
9
|
+
use PhpMcp\Server\Attributes\McpTool;
|
|
10
|
+
use PhpMcp\Server\Attributes\Schema;
|
|
11
|
+
use RuntimeException;
|
|
12
|
+
use JsonException;
|
|
13
|
+
|
|
14
|
+
final class WeatherTools
|
|
15
|
+
{
|
|
16
|
+
#[McpTool(
|
|
17
|
+
name: 'get-weathers',
|
|
18
|
+
description: 'Returns current temperature for a city'
|
|
19
|
+
)]
|
|
20
|
+
public function getWeather(
|
|
21
|
+
#[Schema(type: 'string', minLength: 1, description: 'City name')]
|
|
22
|
+
string $city
|
|
23
|
+
): string {
|
|
24
|
+
$http = new Client([
|
|
25
|
+
'timeout' => 10,
|
|
26
|
+
'connect_timeout' => 8,
|
|
27
|
+
'http_errors' => true,
|
|
28
|
+
'headers' => ['Accept' => 'application/json'],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
$geoUrl = 'https://geocoding-api.open-meteo.com/v1/search'
|
|
33
|
+
. '?name=' . rawurlencode($city)
|
|
34
|
+
. '&count=1&language=en&format=json';
|
|
35
|
+
|
|
36
|
+
$geoRes = $http->get($geoUrl);
|
|
37
|
+
$geoJson = $geoRes->getBody()->getContents();
|
|
38
|
+
$geo = json_decode($geoJson, true, 512, JSON_THROW_ON_ERROR);
|
|
39
|
+
|
|
40
|
+
if (empty($geo['results'][0])) {
|
|
41
|
+
throw new RuntimeException("City \"$city\" not found (geocoding returned no results).");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$lat = (float) $geo['results'][0]['latitude'];
|
|
45
|
+
$lon = (float) $geo['results'][0]['longitude'];
|
|
46
|
+
|
|
47
|
+
$wxUrl = "https://api.open-meteo.com/v1/forecast?latitude={$lat}&longitude={$lon}¤t_weather=true";
|
|
48
|
+
|
|
49
|
+
$wxRes = $http->get($wxUrl);
|
|
50
|
+
$wxJson = $wxRes->getBody()->getContents();
|
|
51
|
+
$wx = json_decode($wxJson, true, 512, JSON_THROW_ON_ERROR);
|
|
52
|
+
|
|
53
|
+
$cw = $wx['current_weather'] ?? null;
|
|
54
|
+
if (!is_array($cw)) {
|
|
55
|
+
throw new RuntimeException('No current weather data found in API response.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
$map = [
|
|
59
|
+
0 => 'Clear sky',
|
|
60
|
+
1 => 'Mainly clear',
|
|
61
|
+
2 => 'Partly cloudy',
|
|
62
|
+
3 => 'Overcast',
|
|
63
|
+
45 => 'Fog',
|
|
64
|
+
48 => 'Depositing rime fog',
|
|
65
|
+
51 => 'Light drizzle',
|
|
66
|
+
53 => 'Moderate drizzle',
|
|
67
|
+
55 => 'Dense drizzle',
|
|
68
|
+
56 => 'Light freezing drizzle',
|
|
69
|
+
57 => 'Dense freezing drizzle',
|
|
70
|
+
61 => 'Slight rain',
|
|
71
|
+
63 => 'Moderate rain',
|
|
72
|
+
65 => 'Heavy rain',
|
|
73
|
+
66 => 'Light freezing rain',
|
|
74
|
+
67 => 'Heavy freezing rain',
|
|
75
|
+
71 => 'Slight snow fall',
|
|
76
|
+
73 => 'Moderate snow fall',
|
|
77
|
+
75 => 'Heavy snow fall',
|
|
78
|
+
77 => 'Snow grains',
|
|
79
|
+
80 => 'Slight rain showers',
|
|
80
|
+
81 => 'Moderate rain showers',
|
|
81
|
+
82 => 'Violent rain showers',
|
|
82
|
+
85 => 'Slight snow showers',
|
|
83
|
+
86 => 'Heavy snow showers',
|
|
84
|
+
95 => 'Thunderstorm',
|
|
85
|
+
96 => 'Thunderstorm with slight hail',
|
|
86
|
+
99 => 'Thunderstorm with heavy hail',
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
$code = (int)($cw['weathercode'] ?? -1);
|
|
90
|
+
$tempC = $cw['temperature'] ?? null;
|
|
91
|
+
|
|
92
|
+
if (!is_numeric($tempC)) {
|
|
93
|
+
throw new RuntimeException('Temperature missing or not numeric in current weather payload.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$desc = $map[$code] ?? 'Unknown';
|
|
97
|
+
return "🌡️ {$tempC} °C · {$desc} (code {$code})";
|
|
98
|
+
} catch (GuzzleException $e) {
|
|
99
|
+
throw new RuntimeException('Weather request failed: ' . $e->getMessage(), previous: $e);
|
|
100
|
+
} catch (JsonException $e) {
|
|
101
|
+
throw new RuntimeException('Weather JSON parsing failed: ' . $e->getMessage(), previous: $e);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\MCP;
|
|
6
|
+
|
|
7
|
+
$root = dirname(__DIR__, 3);
|
|
8
|
+
require $root . '/vendor/autoload.php';
|
|
9
|
+
require $root . '/settings/paths.php';
|
|
10
|
+
|
|
11
|
+
use Dotenv\Dotenv;
|
|
12
|
+
use PhpMcp\Server\Server;
|
|
13
|
+
use PhpMcp\Server\Transports\StreamableHttpServerTransport;
|
|
14
|
+
use Throwable;
|
|
15
|
+
|
|
16
|
+
// ── Load .env (optional) and timezone ──────────────────────────────────────────
|
|
17
|
+
if (file_exists(DOCUMENT_PATH . '/.env')) {
|
|
18
|
+
Dotenv::createImmutable(DOCUMENT_PATH)->safeLoad();
|
|
19
|
+
}
|
|
20
|
+
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'UTC');
|
|
21
|
+
|
|
22
|
+
// ── Resolve settings (with sane defaults) ─────────────────────────────────────
|
|
23
|
+
$appName = $_ENV['MCP_NAME'] ?? 'prisma-php-mcp';
|
|
24
|
+
$appVersion = $_ENV['MCP_VERSION'] ?? '0.0.1';
|
|
25
|
+
$host = $_ENV['MCP_HOST'] ?? '127.0.0.1';
|
|
26
|
+
$port = (int)($_ENV['MCP_PORT'] ?? 4000);
|
|
27
|
+
$prefix = trim($_ENV['MCP_PATH_PREFIX'] ?? 'mcp', '/');
|
|
28
|
+
$enableJson = filter_var($_ENV['MCP_JSON_RESPONSE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
|
29
|
+
|
|
30
|
+
// ── Build server and discover tools ───────────────────────────────────────────
|
|
31
|
+
$server = Server::make()
|
|
32
|
+
->withServerInfo($appName, $appVersion)
|
|
33
|
+
->build();
|
|
34
|
+
|
|
35
|
+
// Scan your source tree for #[McpTool] classes
|
|
36
|
+
$server->discover(DOCUMENT_PATH, ['src']);
|
|
37
|
+
|
|
38
|
+
// ── Pretty console output ─────────────────────────────────────────────────────
|
|
39
|
+
$pid = getmypid() ?: 0;
|
|
40
|
+
$base = "http://{$host}:{$port}/{$prefix}";
|
|
41
|
+
$color = static fn(string $t, string $c) => "\033[{$c}m{$t}\033[0m";
|
|
42
|
+
|
|
43
|
+
echo PHP_EOL;
|
|
44
|
+
echo $color("⚙ {$appName} (v{$appVersion})", '1;36') . PHP_EOL;
|
|
45
|
+
echo $color("→ MCP server starting…", '33') . PHP_EOL;
|
|
46
|
+
echo " Host: {$host}" . PHP_EOL;
|
|
47
|
+
echo " Port: {$port}" . PHP_EOL;
|
|
48
|
+
echo " Path: /{$prefix}" . PHP_EOL;
|
|
49
|
+
echo " JSON resp: " . ($enableJson ? 'enabled' : 'disabled') . PHP_EOL;
|
|
50
|
+
echo " PID: {$pid}" . PHP_EOL;
|
|
51
|
+
echo " URL: {$base}" . PHP_EOL;
|
|
52
|
+
|
|
53
|
+
// ── Graceful shutdown (if pcntl is available) ─────────────────────────────────
|
|
54
|
+
if (function_exists('pcntl_signal')) {
|
|
55
|
+
$stop = function (int $sig) use ($color) {
|
|
56
|
+
echo PHP_EOL . $color("⏹ Caught signal {$sig}. Shutting down…", '33') . PHP_EOL;
|
|
57
|
+
exit(0);
|
|
58
|
+
};
|
|
59
|
+
pcntl_signal(SIGINT, $stop);
|
|
60
|
+
pcntl_signal(SIGTERM, $stop);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Listen ────────────────────────────────────────────────────────────────────
|
|
64
|
+
try {
|
|
65
|
+
$transport = new StreamableHttpServerTransport(
|
|
66
|
+
$host,
|
|
67
|
+
$port,
|
|
68
|
+
$prefix,
|
|
69
|
+
null, // sslContext
|
|
70
|
+
true, // logger
|
|
71
|
+
$enableJson // enableJsonResponse
|
|
72
|
+
// , false // (optional) stateless
|
|
73
|
+
);
|
|
74
|
+
echo $color("✓ Listening on {$base}", '32') . PHP_EOL;
|
|
75
|
+
$server->listen($transport);
|
|
76
|
+
echo PHP_EOL . $color('✔ Server stopped.', '32') . PHP_EOL;
|
|
77
|
+
} catch (Throwable $e) {
|
|
78
|
+
fwrite(STDERR, $color('✖ Server error: ', '31') . $e->getMessage() . PHP_EOL);
|
|
79
|
+
exit(1);
|
|
80
|
+
}
|
|
@@ -20,6 +20,8 @@ class MainLayout
|
|
|
20
20
|
private static ?Set $footerScripts = null;
|
|
21
21
|
private static array $customMetadata = [];
|
|
22
22
|
|
|
23
|
+
private static array $processedScripts = [];
|
|
24
|
+
|
|
23
25
|
public static function init(): void
|
|
24
26
|
{
|
|
25
27
|
if (self::$headScripts === null) {
|
|
@@ -28,6 +30,7 @@ class MainLayout
|
|
|
28
30
|
if (self::$footerScripts === null) {
|
|
29
31
|
self::$footerScripts = new Set();
|
|
30
32
|
}
|
|
33
|
+
self::$processedScripts = [];
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -55,11 +58,19 @@ class MainLayout
|
|
|
55
58
|
$callerClass = $trace[1]['class'] ?? 'Unknown';
|
|
56
59
|
|
|
57
60
|
foreach ($scripts as $script) {
|
|
61
|
+
$scriptKey = md5(trim($script));
|
|
62
|
+
|
|
58
63
|
if (strpos($script, '<script') !== false) {
|
|
59
64
|
$taggedScript = "<!-- class:" . $callerClass . " -->\n" . $script;
|
|
60
|
-
self::$
|
|
65
|
+
if (!isset(self::$processedScripts[$scriptKey])) {
|
|
66
|
+
self::$footerScripts->add($taggedScript);
|
|
67
|
+
self::$processedScripts[$scriptKey] = true;
|
|
68
|
+
}
|
|
61
69
|
} else {
|
|
62
|
-
self::$
|
|
70
|
+
if (!isset(self::$processedScripts[$scriptKey])) {
|
|
71
|
+
self::$footerScripts->add($script);
|
|
72
|
+
self::$processedScripts[$scriptKey] = true;
|
|
73
|
+
}
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
}
|
|
@@ -97,6 +108,7 @@ class MainLayout
|
|
|
97
108
|
public static function outputFooterScripts(): string
|
|
98
109
|
{
|
|
99
110
|
$processed = [];
|
|
111
|
+
$componentCounter = 0;
|
|
100
112
|
|
|
101
113
|
foreach (self::$footerScripts->values() as $script) {
|
|
102
114
|
if (preg_match('/<!-- class:([^\s]+) -->/', $script, $matches)) {
|
|
@@ -106,9 +118,11 @@ class MainLayout
|
|
|
106
118
|
if (str_starts_with(trim($script), '<script')) {
|
|
107
119
|
$script = preg_replace_callback(
|
|
108
120
|
'/<script\b([^>]*)>/i',
|
|
109
|
-
function ($m) use ($rawClassName) {
|
|
121
|
+
function ($m) use ($rawClassName, &$componentCounter) {
|
|
110
122
|
$attrs = $m[1];
|
|
111
|
-
$
|
|
123
|
+
$scriptHash = substr(md5($m[0]), 0, 8);
|
|
124
|
+
$encodedClass = 's' . base_convert(sprintf('%u', crc32($rawClassName . $componentCounter . $scriptHash)), 10, 36);
|
|
125
|
+
$componentCounter++;
|
|
112
126
|
|
|
113
127
|
if (!str_contains($attrs, 'pp-component=')) {
|
|
114
128
|
$attrs .= " pp-component=\"{$encodedClass}\"";
|
|
@@ -150,6 +164,7 @@ class MainLayout
|
|
|
150
164
|
public static function clearFooterScripts(): void
|
|
151
165
|
{
|
|
152
166
|
self::$footerScripts->clear();
|
|
167
|
+
self::$processedScripts = [];
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
/**
|