create-prisma-php-app 3.5.4 β 3.6.1
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 +12 -7
- package/dist/index.js +41 -30
- package/dist/settings/auto-swagger-docs.ts +195 -94
- package/dist/settings/bs-config.ts +53 -58
- package/dist/settings/project-name.ts +2 -0
- 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/MCP/WeatherTools.php +104 -0
- package/dist/src/Lib/MCP/mcp-server.php +80 -0
- package/dist/src/Lib/Middleware/AuthMiddleware.php +6 -3
- package/dist/src/Lib/Middleware/CorsMiddleware.php +145 -0
- package/dist/src/Lib/Websocket/websocket-server.php +105 -14
- package/package.json +1 -1
- package/dist/settings/restart-websocket.bat +0 -28
|
@@ -1,53 +1,52 @@
|
|
|
1
|
-
import { spawn, ChildProcess } from "child_process";
|
|
2
1
|
import { join } from "path";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import {
|
|
3
|
+
createRestartableProcess,
|
|
4
|
+
createSrcWatcher,
|
|
5
|
+
DebouncedWorker,
|
|
6
|
+
DEFAULT_AWF,
|
|
7
|
+
onExit,
|
|
8
|
+
} from "./utils.js";
|
|
9
|
+
|
|
10
|
+
// Config
|
|
11
|
+
const phpPath = process.env.PHP_PATH ?? "php";
|
|
12
|
+
const SRC_DIR = join(process.cwd(), "src");
|
|
9
13
|
const serverScriptPath = join(
|
|
10
|
-
|
|
11
|
-
"..",
|
|
12
|
-
"src",
|
|
14
|
+
SRC_DIR,
|
|
13
15
|
"Lib",
|
|
14
16
|
"Websocket",
|
|
15
17
|
"websocket-server.php"
|
|
16
18
|
);
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
serverProcess.on("close", (code: number) => {
|
|
39
|
-
console.log(`WebSocket server exited with code ${code}`);
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Initial start
|
|
44
|
-
restartServer();
|
|
20
|
+
// Restartable WS server
|
|
21
|
+
const ws = createRestartableProcess({
|
|
22
|
+
name: "WebSocket",
|
|
23
|
+
cmd: phpPath,
|
|
24
|
+
args: [serverScriptPath],
|
|
25
|
+
windowsKillTree: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
ws.start();
|
|
29
|
+
|
|
30
|
+
// Debounced restarter
|
|
31
|
+
const restarter = new DebouncedWorker(
|
|
32
|
+
async () => {
|
|
33
|
+
await ws.restart("file change");
|
|
34
|
+
},
|
|
35
|
+
400,
|
|
36
|
+
"ws-restart"
|
|
37
|
+
);
|
|
45
38
|
|
|
46
|
-
// Watch
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
// Watch ./src recursively; restart on code/data file changes
|
|
40
|
+
createSrcWatcher(SRC_DIR, {
|
|
41
|
+
exts: [".php", ".ts", ".js", ".json"],
|
|
42
|
+
onEvent: (ev, _abs, rel) => restarter.schedule(`${ev}: ${rel}`),
|
|
43
|
+
awaitWriteFinish: DEFAULT_AWF,
|
|
44
|
+
logPrefix: "WS watch",
|
|
45
|
+
usePolling: true,
|
|
46
|
+
interval: 1000,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Graceful shutdown
|
|
50
|
+
onExit(async () => {
|
|
51
|
+
await ws.stop();
|
|
52
|
+
});
|
package/dist/settings/utils.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { fileURLToPath } from "url";
|
|
2
2
|
import { dirname } from "path";
|
|
3
|
+
import chokidar, { FSWatcher } from "chokidar";
|
|
4
|
+
import { spawn, ChildProcess, execFile } from "child_process";
|
|
5
|
+
import { relative } from "path";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Retrieves the file metadata including the filename and directory name.
|
|
@@ -12,3 +15,240 @@ export function getFileMeta() {
|
|
|
12
15
|
const __dirname = dirname(__filename);
|
|
13
16
|
return { __filename, __dirname };
|
|
14
17
|
}
|
|
18
|
+
|
|
19
|
+
export type WatchEvent = "add" | "addDir" | "change" | "unlink" | "unlinkDir";
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_IGNORES: (string | RegExp)[] = [
|
|
22
|
+
/(^|[\/\\])\../, // dotfiles
|
|
23
|
+
"**/node_modules/**",
|
|
24
|
+
"**/vendor/**",
|
|
25
|
+
"**/dist/**",
|
|
26
|
+
"**/build/**",
|
|
27
|
+
"**/.cache/**",
|
|
28
|
+
"**/*.log",
|
|
29
|
+
"**/*.tmp",
|
|
30
|
+
"**/*.swp",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_AWF = { stabilityThreshold: 300, pollInterval: 100 };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a chokidar watcher for a given root. Optionally filter by file extensions.
|
|
37
|
+
*/
|
|
38
|
+
export function createSrcWatcher(
|
|
39
|
+
root: string,
|
|
40
|
+
opts: {
|
|
41
|
+
exts?: string[]; // e.g. ['.php','.ts']
|
|
42
|
+
onEvent: (event: WatchEvent, absPath: string, relPath: string) => void;
|
|
43
|
+
ignored?: (string | RegExp)[];
|
|
44
|
+
awaitWriteFinish?: { stabilityThreshold: number; pollInterval: number };
|
|
45
|
+
logPrefix?: string;
|
|
46
|
+
usePolling?: boolean;
|
|
47
|
+
interval?: number;
|
|
48
|
+
}
|
|
49
|
+
): FSWatcher {
|
|
50
|
+
const {
|
|
51
|
+
exts,
|
|
52
|
+
onEvent,
|
|
53
|
+
ignored = DEFAULT_IGNORES,
|
|
54
|
+
awaitWriteFinish = DEFAULT_AWF,
|
|
55
|
+
logPrefix = "watch",
|
|
56
|
+
usePolling = true,
|
|
57
|
+
} = opts;
|
|
58
|
+
|
|
59
|
+
const watcher = chokidar.watch(root, {
|
|
60
|
+
ignoreInitial: true,
|
|
61
|
+
persistent: true,
|
|
62
|
+
ignored,
|
|
63
|
+
awaitWriteFinish,
|
|
64
|
+
usePolling,
|
|
65
|
+
interval: opts.interval ?? 1000,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
watcher
|
|
69
|
+
.on("ready", () => {
|
|
70
|
+
console.log(`[${logPrefix}] Watching ${root.replace(/\\/g, "/")}/**/*`);
|
|
71
|
+
})
|
|
72
|
+
.on("all", (event: WatchEvent, filePath: string) => {
|
|
73
|
+
// Optional extension filter
|
|
74
|
+
if (exts && exts.length > 0) {
|
|
75
|
+
const ok = exts.some((ext) => filePath.endsWith(ext));
|
|
76
|
+
if (!ok) return;
|
|
77
|
+
}
|
|
78
|
+
const rel = relative(root, filePath).replace(/\\/g, "/");
|
|
79
|
+
if (event === "add" || event === "change" || event === "unlink") {
|
|
80
|
+
onEvent(event, filePath, rel);
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.on("error", (err) => console.error(`[${logPrefix}] Error:`, err));
|
|
84
|
+
|
|
85
|
+
return watcher;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Debounced worker that ensures only one run at a time; extra runs get queued once.
|
|
90
|
+
*/
|
|
91
|
+
export class DebouncedWorker {
|
|
92
|
+
private timer: NodeJS.Timeout | null = null;
|
|
93
|
+
private running = false;
|
|
94
|
+
private queued = false;
|
|
95
|
+
|
|
96
|
+
constructor(
|
|
97
|
+
private work: () => Promise<void> | void,
|
|
98
|
+
private debounceMs = 350,
|
|
99
|
+
private name = "worker"
|
|
100
|
+
) {}
|
|
101
|
+
|
|
102
|
+
schedule(reason?: string) {
|
|
103
|
+
if (reason) console.log(`[${this.name}] ${reason} β scheduled`);
|
|
104
|
+
if (this.timer) clearTimeout(this.timer);
|
|
105
|
+
this.timer = setTimeout(() => {
|
|
106
|
+
this.timer = null;
|
|
107
|
+
this.runNow().catch(() => {});
|
|
108
|
+
}, this.debounceMs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async runNow() {
|
|
112
|
+
if (this.running) {
|
|
113
|
+
this.queued = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.running = true;
|
|
117
|
+
try {
|
|
118
|
+
await this.work();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(`[${this.name}] error:`, err);
|
|
121
|
+
} finally {
|
|
122
|
+
this.running = false;
|
|
123
|
+
if (this.queued) {
|
|
124
|
+
this.queued = false;
|
|
125
|
+
this.runNow().catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Cross-platform restartable process.
|
|
133
|
+
*/
|
|
134
|
+
export function createRestartableProcess(spec: {
|
|
135
|
+
name: string;
|
|
136
|
+
cmd: string;
|
|
137
|
+
args?: string[];
|
|
138
|
+
stdio?: "inherit" | [any, any, any];
|
|
139
|
+
gracefulSignal?: NodeJS.Signals;
|
|
140
|
+
forceKillAfterMs?: number;
|
|
141
|
+
windowsKillTree?: boolean;
|
|
142
|
+
onStdout?: (buf: Buffer) => void;
|
|
143
|
+
onStderr?: (buf: Buffer) => void;
|
|
144
|
+
}) {
|
|
145
|
+
const {
|
|
146
|
+
name,
|
|
147
|
+
cmd,
|
|
148
|
+
args = [],
|
|
149
|
+
stdio = ["ignore", "pipe", "pipe"],
|
|
150
|
+
gracefulSignal = "SIGINT",
|
|
151
|
+
forceKillAfterMs = 2000,
|
|
152
|
+
windowsKillTree = true,
|
|
153
|
+
onStdout,
|
|
154
|
+
onStderr,
|
|
155
|
+
} = spec;
|
|
156
|
+
|
|
157
|
+
let child: ChildProcess | null = null;
|
|
158
|
+
|
|
159
|
+
function start() {
|
|
160
|
+
console.log(`[${name}] Starting: ${cmd} ${args.join(" ")}`.trim());
|
|
161
|
+
child = spawn(cmd, args, { stdio, windowsHide: true });
|
|
162
|
+
|
|
163
|
+
child.stdout?.on("data", (buf: Buffer) => {
|
|
164
|
+
if (onStdout) onStdout(buf);
|
|
165
|
+
else process.stdout.write(`[${name}] ${buf.toString()}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
child.stderr?.on("data", (buf: Buffer) => {
|
|
169
|
+
if (onStderr) onStderr(buf);
|
|
170
|
+
else process.stderr.write(`[${name}:err] ${buf.toString()}`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
child.on("close", (code) => {
|
|
174
|
+
console.log(`[${name}] Exited with code ${code}`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
child.on("error", (err) => {
|
|
178
|
+
console.error(`[${name}] Failed to start:`, err);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return child;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function killOnWindows(pid: number): Promise<void> {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
const cp = execFile("taskkill", ["/F", "/T", "/PID", String(pid)], () =>
|
|
187
|
+
resolve()
|
|
188
|
+
);
|
|
189
|
+
cp.on("error", () => resolve());
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function stop(): Promise<void> {
|
|
194
|
+
if (!child || child.killed) return;
|
|
195
|
+
const pid = child.pid!;
|
|
196
|
+
console.log(`[${name}] Stoppingβ¦`);
|
|
197
|
+
|
|
198
|
+
if (process.platform === "win32" && windowsKillTree) {
|
|
199
|
+
await killOnWindows(pid);
|
|
200
|
+
child = null;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await new Promise<void>((resolve) => {
|
|
205
|
+
const done = () => resolve();
|
|
206
|
+
child!.once("close", done).once("exit", done).once("disconnect", done);
|
|
207
|
+
try {
|
|
208
|
+
child!.kill(gracefulSignal);
|
|
209
|
+
} catch {
|
|
210
|
+
resolve();
|
|
211
|
+
}
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
if (child && !child.killed) {
|
|
214
|
+
try {
|
|
215
|
+
process.kill(pid, "SIGKILL");
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
}, forceKillAfterMs);
|
|
219
|
+
});
|
|
220
|
+
child = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function restart(reason?: string) {
|
|
224
|
+
if (reason) console.log(`[${name}] Restart requested: ${reason}`);
|
|
225
|
+
await stop();
|
|
226
|
+
return start();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getChild() {
|
|
230
|
+
return child;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { start, stop, restart, getChild };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Register shutdown cleanup callbacks.
|
|
238
|
+
*/
|
|
239
|
+
export function onExit(fn: () => Promise<void> | void) {
|
|
240
|
+
const wrap = (sig: string) => async () => {
|
|
241
|
+
console.log(`[proc] Received ${sig}, shutting downβ¦`);
|
|
242
|
+
try {
|
|
243
|
+
await fn();
|
|
244
|
+
} finally {
|
|
245
|
+
process.exit(0);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
process.on("SIGINT", wrap("SIGINT"));
|
|
249
|
+
process.on("SIGTERM", wrap("SIGTERM"));
|
|
250
|
+
process.on("uncaughtException", async (err) => {
|
|
251
|
+
console.error("[proc] Uncaught exception:", err);
|
|
252
|
+
await wrap("uncaughtException")();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -12,6 +12,9 @@ final class AuthMiddleware
|
|
|
12
12
|
{
|
|
13
13
|
public static function handle($requestPathname)
|
|
14
14
|
{
|
|
15
|
+
$singInRoute = "/signin";
|
|
16
|
+
$deniedRoute = "/denied";
|
|
17
|
+
|
|
15
18
|
if (AuthConfig::IS_ALL_ROUTES_PRIVATE) {
|
|
16
19
|
$isLogin = Auth::getInstance()->isAuthenticated();
|
|
17
20
|
$isApiAuthRoute = stripos($requestPathname, AuthConfig::API_AUTH_PREFIX) === 0;
|
|
@@ -42,7 +45,7 @@ final class AuthMiddleware
|
|
|
42
45
|
|
|
43
46
|
// Redirect to the default home route if the user is already authenticated
|
|
44
47
|
if (!$isLogin && !$isPublicRoute) {
|
|
45
|
-
Request::redirect(
|
|
48
|
+
Request::redirect($singInRoute);
|
|
46
49
|
}
|
|
47
50
|
} else {
|
|
48
51
|
// Skip the middleware if the route is public
|
|
@@ -52,7 +55,7 @@ final class AuthMiddleware
|
|
|
52
55
|
|
|
53
56
|
// Check if the user is authorized to access the route or redirect to login
|
|
54
57
|
if (!self::isAuthorized()) {
|
|
55
|
-
Request::redirect(
|
|
58
|
+
Request::redirect($singInRoute);
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -65,7 +68,7 @@ final class AuthMiddleware
|
|
|
65
68
|
// echo "You are authorized to access this route";
|
|
66
69
|
} elseif ($matchValue === "Role mismatch") {
|
|
67
70
|
// echo "You are not authorized to access this route";
|
|
68
|
-
Request::redirect(
|
|
71
|
+
Request::redirect($deniedRoute);
|
|
69
72
|
} else {
|
|
70
73
|
// echo "Unexpected error encountered";
|
|
71
74
|
}
|