synthux-mcp 2.1.0 → 2.1.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/README.md +7 -1
- package/apple-fm-bridge.swift +277 -0
- package/dist/apple-fm-bridge.swift +277 -0
- package/dist/cli.js +600 -12
- package/package.json +2 -1
- package/server.js +377 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "synthux-mcp",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Official Model Context Protocol (MCP) server & Chrome extension bridge for synthux — AI-powered UX & WCAG 2.2 AA auditing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dist/",
|
|
12
12
|
"rules/",
|
|
13
13
|
"server.js",
|
|
14
|
+
"apple-fm-bridge.swift",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
package/server.js
CHANGED
|
@@ -22,7 +22,9 @@ import http from 'node:http';
|
|
|
22
22
|
import crypto from 'node:crypto';
|
|
23
23
|
import readline from 'node:readline';
|
|
24
24
|
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
25
26
|
import path from 'node:path';
|
|
27
|
+
import { execFile, spawn } from 'node:child_process';
|
|
26
28
|
import { fileURLToPath } from 'node:url';
|
|
27
29
|
|
|
28
30
|
import {
|
|
@@ -57,7 +59,7 @@ const PORT = Number(process.env.SYNTHUX_MCP_PORT || 17483);
|
|
|
57
59
|
const REQUIRED_TOKEN = (process.env.SYNTHUX_MCP_TOKEN || '').trim();
|
|
58
60
|
const SERVER_INFO = {
|
|
59
61
|
name: 'synthux-mcp',
|
|
60
|
-
version: '2.1.
|
|
62
|
+
version: '2.1.1'
|
|
61
63
|
};
|
|
62
64
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
63
65
|
|
|
@@ -113,6 +115,195 @@ function validateSafeHttpUrl(urlStr) {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
// ─── Apple Foundation Models (macOS 26 / macOS 27+) Local Bridge ─────────────
|
|
119
|
+
|
|
120
|
+
const APPLE_FM_SWIFT_PATH = [
|
|
121
|
+
path.join(__dirname, 'apple-fm-bridge.swift'),
|
|
122
|
+
path.join(__dirname, '..', 'apple-fm-bridge.swift'),
|
|
123
|
+
path.join(ROOT_DIR, 'mcp', 'apple-fm-bridge.swift')
|
|
124
|
+
].find(p => fs.existsSync(p)) || path.join(__dirname, 'apple-fm-bridge.swift');
|
|
125
|
+
|
|
126
|
+
const APPLE_FM_CACHE_DIR = path.join(os.tmpdir(), 'synthux-apple-fm-cache');
|
|
127
|
+
const APPLE_FM_BIN_PATH = path.join(APPLE_FM_CACHE_DIR, 'synthux-apple-fm');
|
|
128
|
+
let _appleFmCompiling = null;
|
|
129
|
+
|
|
130
|
+
async function ensureAppleFmBinary() {
|
|
131
|
+
if (process.platform !== 'darwin') return null;
|
|
132
|
+
if (!fs.existsSync(APPLE_FM_SWIFT_PATH)) return null;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
fs.mkdirSync(APPLE_FM_CACHE_DIR, { recursive: true });
|
|
136
|
+
if (fs.existsSync(APPLE_FM_BIN_PATH)) {
|
|
137
|
+
const srcStat = fs.statSync(APPLE_FM_SWIFT_PATH);
|
|
138
|
+
const binStat = fs.statSync(APPLE_FM_BIN_PATH);
|
|
139
|
+
if (binStat.mtimeMs >= srcStat.mtimeMs) {
|
|
140
|
+
return APPLE_FM_BIN_PATH;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {}
|
|
144
|
+
|
|
145
|
+
if (_appleFmCompiling) return _appleFmCompiling;
|
|
146
|
+
|
|
147
|
+
_appleFmCompiling = new Promise((resolve) => {
|
|
148
|
+
const moduleCache = path.join(APPLE_FM_CACHE_DIR, 'swift-module-cache');
|
|
149
|
+
try { fs.mkdirSync(moduleCache, { recursive: true }); } catch {}
|
|
150
|
+
|
|
151
|
+
execFile(
|
|
152
|
+
'swiftc',
|
|
153
|
+
['-module-cache-path', moduleCache, '-O', APPLE_FM_SWIFT_PATH, '-o', APPLE_FM_BIN_PATH],
|
|
154
|
+
{ timeout: 30000 },
|
|
155
|
+
(err) => {
|
|
156
|
+
_appleFmCompiling = null;
|
|
157
|
+
if (err) {
|
|
158
|
+
resolve(null);
|
|
159
|
+
} else {
|
|
160
|
+
try { fs.chmodSync(APPLE_FM_BIN_PATH, 0o755); } catch {}
|
|
161
|
+
resolve(APPLE_FM_BIN_PATH);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return _appleFmCompiling;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function runAppleFmStatus() {
|
|
171
|
+
if (process.platform !== 'darwin') {
|
|
172
|
+
return {
|
|
173
|
+
available: false,
|
|
174
|
+
status: 'unsupported_os',
|
|
175
|
+
reason: 'Apple Foundation Models require macOS 26/27 on Apple Silicon.',
|
|
176
|
+
osVersion: `${process.platform} (${os.release()})`,
|
|
177
|
+
models: []
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const bin = await ensureAppleFmBinary();
|
|
182
|
+
if (!bin) {
|
|
183
|
+
return {
|
|
184
|
+
available: false,
|
|
185
|
+
status: 'compiler_unavailable',
|
|
186
|
+
reason: 'Swift / Xcode Command Line Tools are required to bridge Apple FoundationModels on macOS.',
|
|
187
|
+
osVersion: `macOS (${os.release()})`,
|
|
188
|
+
models: [
|
|
189
|
+
{ id: 'apple-on-device', name: 'Apple Foundation Model (On-Device ANE)', size: 0 },
|
|
190
|
+
{ id: 'apple-pcc', name: 'Apple Private Cloud Compute (macOS 27)', size: 0 },
|
|
191
|
+
{ id: 'apple-content-tagging', name: 'Apple Foundation Model (Fast Tagging)', size: 0 }
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
execFile(bin, ['status'], { timeout: 10000 }, (err, stdout) => {
|
|
198
|
+
if (err || !stdout) {
|
|
199
|
+
resolve({
|
|
200
|
+
available: false,
|
|
201
|
+
status: 'error',
|
|
202
|
+
reason: err?.message || 'Failed to query Apple Foundation Models status.',
|
|
203
|
+
osVersion: `macOS (${os.release()})`,
|
|
204
|
+
models: [
|
|
205
|
+
{ id: 'apple-on-device', name: 'Apple Foundation Model (On-Device ANE)', size: 0 },
|
|
206
|
+
{ id: 'apple-pcc', name: 'Apple Private Cloud Compute (macOS 27)', size: 0 },
|
|
207
|
+
{ id: 'apple-content-tagging', name: 'Apple Foundation Model (Fast Tagging)', size: 0 }
|
|
208
|
+
]
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
resolve(JSON.parse(stdout.trim()));
|
|
214
|
+
} catch {
|
|
215
|
+
resolve({
|
|
216
|
+
available: false,
|
|
217
|
+
status: 'parse_error',
|
|
218
|
+
raw: stdout.trim(),
|
|
219
|
+
models: []
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function runAppleFmGenerate(payload) {
|
|
227
|
+
if (process.platform !== 'darwin') {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: 'Apple Foundation Models require macOS 26/27 on Apple Silicon.'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const bin = await ensureAppleFmBinary();
|
|
235
|
+
if (!bin) {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
error: 'Unable to compile Apple FoundationModels Swift bridge. Ensure Xcode / Command Line Tools are installed.'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
const child = spawn(bin, ['generate'], {
|
|
244
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
let stdout = '';
|
|
248
|
+
let stderr = '';
|
|
249
|
+
const timer = setTimeout(() => {
|
|
250
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
251
|
+
resolve({
|
|
252
|
+
success: false,
|
|
253
|
+
error: 'Apple Foundation Model request timed out.'
|
|
254
|
+
});
|
|
255
|
+
}, 60000);
|
|
256
|
+
|
|
257
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
258
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
259
|
+
|
|
260
|
+
child.on('close', () => {
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
try {
|
|
263
|
+
const parsed = JSON.parse(stdout.trim());
|
|
264
|
+
resolve(parsed);
|
|
265
|
+
} catch {
|
|
266
|
+
resolve({
|
|
267
|
+
success: false,
|
|
268
|
+
error: stderr.trim() || stdout.trim() || 'Apple Foundation Model returned an invalid response.'
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
child.on('error', (err) => {
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
resolve({
|
|
276
|
+
success: false,
|
|
277
|
+
error: `Apple Foundation Model process error: ${err.message}`
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
child.stdin.write(JSON.stringify(payload));
|
|
282
|
+
child.stdin.end();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function readJsonBody(req, maxBytes = 10 * 1024 * 1024) {
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
288
|
+
let body = '';
|
|
289
|
+
req.on('data', (chunk) => {
|
|
290
|
+
body += chunk.toString('utf8');
|
|
291
|
+
if (body.length > maxBytes) {
|
|
292
|
+
req.destroy?.();
|
|
293
|
+
reject(new Error('Payload too large'));
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
req.on('end', () => {
|
|
297
|
+
try {
|
|
298
|
+
resolve(body ? JSON.parse(body) : {});
|
|
299
|
+
} catch (err) {
|
|
300
|
+
reject(err);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
req.on('error', reject);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
116
307
|
// ─── RFC 6455 Minimal WebSocket Server Implementation ────────────────────────
|
|
117
308
|
|
|
118
309
|
class WebSocketConnection {
|
|
@@ -337,7 +528,7 @@ export const MCP_TOOLS = [
|
|
|
337
528
|
},
|
|
338
529
|
provider: {
|
|
339
530
|
type: 'string',
|
|
340
|
-
enum: ['ollama', 'gemini', 'openai', 'claude'],
|
|
531
|
+
enum: ['ollama', 'apple', 'chromeai', 'gemini', 'openai', 'claude'],
|
|
341
532
|
description: 'Optional AI provider override'
|
|
342
533
|
},
|
|
343
534
|
model: {
|
|
@@ -545,7 +736,7 @@ export const MCP_TOOLS = [
|
|
|
545
736
|
},
|
|
546
737
|
{
|
|
547
738
|
name: 'synthux_configure_ai',
|
|
548
|
-
description: 'Read or update synthux AI provider configuration (Local Ollama
|
|
739
|
+
description: 'Read or update synthux AI provider configuration (Local Ollama, Apple Intelligence macOS 26/27 FoundationModels, Chrome Built-in AI Gemini Nano, or Cloud Gemini/OpenAI/Claude) and test provider connectivity.',
|
|
549
740
|
inputSchema: {
|
|
550
741
|
type: 'object',
|
|
551
742
|
properties: {
|
|
@@ -554,7 +745,7 @@ export const MCP_TOOLS = [
|
|
|
554
745
|
enum: ['get', 'update', 'ping'],
|
|
555
746
|
default: 'get'
|
|
556
747
|
},
|
|
557
|
-
provider: { type: 'string', enum: ['ollama', 'gemini', 'openai', 'claude'] },
|
|
748
|
+
provider: { type: 'string', enum: ['ollama', 'apple', 'chromeai', 'gemini', 'openai', 'claude'] },
|
|
558
749
|
model: { type: 'string' },
|
|
559
750
|
endpoint: { type: 'string' },
|
|
560
751
|
enableVision: { type: 'boolean' },
|
|
@@ -1225,10 +1416,24 @@ async function handleHttpRequest(req, res) {
|
|
|
1225
1416
|
return;
|
|
1226
1417
|
}
|
|
1227
1418
|
|
|
1419
|
+
// Set CORS headers for allowed origins (chrome-extension://* and local CLI)
|
|
1420
|
+
const corsHeaders = {
|
|
1421
|
+
'Access-Control-Allow-Origin': origin || '*',
|
|
1422
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
1423
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-synthux-token',
|
|
1424
|
+
'Content-Type': 'application/json'
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
if (req.method === 'OPTIONS') {
|
|
1428
|
+
res.writeHead(204, corsHeaders);
|
|
1429
|
+
res.end();
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1228
1433
|
if (REQUIRED_TOKEN) {
|
|
1229
1434
|
const headerToken = req.headers['x-synthux-token'] || (req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
1230
1435
|
if (headerToken !== REQUIRED_TOKEN) {
|
|
1231
|
-
res.writeHead(401,
|
|
1436
|
+
res.writeHead(401, corsHeaders);
|
|
1232
1437
|
res.end(JSON.stringify({ error: 'Unauthorized: Invalid SYNTHUX_MCP_TOKEN.' }));
|
|
1233
1438
|
return;
|
|
1234
1439
|
}
|
|
@@ -1237,7 +1442,7 @@ async function handleHttpRequest(req, res) {
|
|
|
1237
1442
|
const url = new URL(req.url || '/', `http://${LOOPBACK_HOST}:${PORT}`);
|
|
1238
1443
|
|
|
1239
1444
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
1240
|
-
res.writeHead(200,
|
|
1445
|
+
res.writeHead(200, corsHeaders);
|
|
1241
1446
|
res.end(JSON.stringify({
|
|
1242
1447
|
status: 'ok',
|
|
1243
1448
|
server: SERVER_INFO,
|
|
@@ -1246,6 +1451,166 @@ async function handleHttpRequest(req, res) {
|
|
|
1246
1451
|
return;
|
|
1247
1452
|
}
|
|
1248
1453
|
|
|
1454
|
+
// ─── Apple Foundation Models (macOS 26 / macOS 27+) Local Endpoints ────────
|
|
1455
|
+
if (req.method === 'GET' && url.pathname === '/apple-fm/status') {
|
|
1456
|
+
const status = await runAppleFmStatus();
|
|
1457
|
+
res.writeHead(200, corsHeaders);
|
|
1458
|
+
res.end(JSON.stringify(status));
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (req.method === 'POST' && url.pathname === '/apple-fm/generate') {
|
|
1463
|
+
try {
|
|
1464
|
+
const payload = await readJsonBody(req);
|
|
1465
|
+
if (!payload.prompt) {
|
|
1466
|
+
res.writeHead(400, corsHeaders);
|
|
1467
|
+
res.end(JSON.stringify({ success: false, error: 'prompt is required' }));
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const result = await runAppleFmGenerate(payload);
|
|
1471
|
+
res.writeHead(result.success ? 200 : 500, corsHeaders);
|
|
1472
|
+
res.end(JSON.stringify(result));
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
res.writeHead(400, corsHeaders);
|
|
1475
|
+
res.end(JSON.stringify({ success: false, error: err.message }));
|
|
1476
|
+
}
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// ─── Backward-Compatible Ollama Endpoints on 127.0.0.1:17483 ──────────────
|
|
1481
|
+
// Allows even the v2.1.0 Chrome Extension to use Apple Foundation Models
|
|
1482
|
+
// simply by setting the Ollama Endpoint to http://127.0.0.1:17483.
|
|
1483
|
+
if (req.method === 'GET' && url.pathname === '/api/tags') {
|
|
1484
|
+
const status = await runAppleFmStatus();
|
|
1485
|
+
res.writeHead(200, corsHeaders);
|
|
1486
|
+
res.end(JSON.stringify({
|
|
1487
|
+
models: (status.models || []).map(m => ({
|
|
1488
|
+
name: m.id,
|
|
1489
|
+
model: m.id,
|
|
1490
|
+
size: m.size || 0,
|
|
1491
|
+
details: { family: 'apple-foundation-model', parameter_size: '3B-ANE' }
|
|
1492
|
+
}))
|
|
1493
|
+
}));
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (req.method === 'GET' && url.pathname === '/api/version') {
|
|
1498
|
+
const status = await runAppleFmStatus();
|
|
1499
|
+
res.writeHead(200, corsHeaders);
|
|
1500
|
+
res.end(JSON.stringify({ version: status.variant || status.osVersion || 'apple-fm-27.0' }));
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (req.method === 'POST' && url.pathname === '/api/show') {
|
|
1505
|
+
res.writeHead(200, corsHeaders);
|
|
1506
|
+
res.end(JSON.stringify({
|
|
1507
|
+
capabilities: ['completion'],
|
|
1508
|
+
details: { family: 'apple-foundation-model' }
|
|
1509
|
+
}));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (req.method === 'POST' && url.pathname === '/api/generate') {
|
|
1514
|
+
try {
|
|
1515
|
+
const body = await readJsonBody(req);
|
|
1516
|
+
if (!body.model) {
|
|
1517
|
+
res.writeHead(400, corsHeaders);
|
|
1518
|
+
res.end(JSON.stringify({ error: 'model is required' }));
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const result = await runAppleFmGenerate({
|
|
1522
|
+
model: body.model,
|
|
1523
|
+
prompt: body.prompt || '',
|
|
1524
|
+
system: body.system || '',
|
|
1525
|
+
temperature: body.options?.temperature ?? 0.3,
|
|
1526
|
+
maxTokens: body.options?.num_predict ?? 2048
|
|
1527
|
+
});
|
|
1528
|
+
if (!result.success) {
|
|
1529
|
+
res.writeHead(500, corsHeaders);
|
|
1530
|
+
res.end(JSON.stringify({ error: result.error || 'Apple Foundation Model error' }));
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
res.writeHead(200, corsHeaders);
|
|
1534
|
+
res.end(JSON.stringify({
|
|
1535
|
+
model: result.model || body.model,
|
|
1536
|
+
response: result.response,
|
|
1537
|
+
done: true,
|
|
1538
|
+
total_duration: result.totalDuration || 0,
|
|
1539
|
+
prompt_eval_count: result.inputTokens || 0,
|
|
1540
|
+
eval_count: result.outputTokens || 0
|
|
1541
|
+
}));
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
res.writeHead(400, corsHeaders);
|
|
1544
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1545
|
+
}
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// ─── OpenAI-Compatible Endpoints on 127.0.0.1:17483 (/v1/*) ───────────────
|
|
1550
|
+
if (req.method === 'GET' && url.pathname === '/v1/models') {
|
|
1551
|
+
const status = await runAppleFmStatus();
|
|
1552
|
+
res.writeHead(200, corsHeaders);
|
|
1553
|
+
res.end(JSON.stringify({
|
|
1554
|
+
object: 'list',
|
|
1555
|
+
data: (status.models || []).map(m => ({
|
|
1556
|
+
id: m.id,
|
|
1557
|
+
object: 'model',
|
|
1558
|
+
owned_by: 'apple-foundation-models'
|
|
1559
|
+
}))
|
|
1560
|
+
}));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (req.method === 'POST' && url.pathname === '/v1/chat/completions') {
|
|
1565
|
+
try {
|
|
1566
|
+
const body = await readJsonBody(req);
|
|
1567
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
1568
|
+
const sysMsg = messages.find(m => m.role === 'system');
|
|
1569
|
+
const userMsg = [...messages].reverse().find(m => m.role === 'user');
|
|
1570
|
+
const userText = Array.isArray(userMsg?.content)
|
|
1571
|
+
? userMsg.content.filter(p => p.type === 'text').map(p => p.text).join('\n')
|
|
1572
|
+
: String(userMsg?.content || '');
|
|
1573
|
+
|
|
1574
|
+
const result = await runAppleFmGenerate({
|
|
1575
|
+
model: body.model || 'apple-on-device',
|
|
1576
|
+
prompt: userText,
|
|
1577
|
+
system: sysMsg?.content || '',
|
|
1578
|
+
temperature: body.temperature ?? 0.3,
|
|
1579
|
+
maxTokens: body.max_tokens ?? 2048
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
if (!result.success) {
|
|
1583
|
+
res.writeHead(500, corsHeaders);
|
|
1584
|
+
res.end(JSON.stringify({ error: { message: result.error } }));
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
res.writeHead(200, corsHeaders);
|
|
1589
|
+
res.end(JSON.stringify({
|
|
1590
|
+
id: `chatcmpl-${Date.now()}`,
|
|
1591
|
+
object: 'chat.completion',
|
|
1592
|
+
created: Math.floor(Date.now() / 1000),
|
|
1593
|
+
model: result.model || body.model || 'apple-on-device',
|
|
1594
|
+
choices: [
|
|
1595
|
+
{
|
|
1596
|
+
index: 0,
|
|
1597
|
+
message: { role: 'assistant', content: result.response },
|
|
1598
|
+
finish_reason: 'stop'
|
|
1599
|
+
}
|
|
1600
|
+
],
|
|
1601
|
+
usage: {
|
|
1602
|
+
prompt_tokens: result.inputTokens || 0,
|
|
1603
|
+
completion_tokens: result.outputTokens || 0,
|
|
1604
|
+
total_tokens: (result.inputTokens || 0) + (result.outputTokens || 0)
|
|
1605
|
+
}
|
|
1606
|
+
}));
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
res.writeHead(400, corsHeaders);
|
|
1609
|
+
res.end(JSON.stringify({ error: { message: err.message } }));
|
|
1610
|
+
}
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1249
1614
|
// Streamable HTTP MCP Endpoint (/mcp)
|
|
1250
1615
|
if (url.pathname === '/mcp') {
|
|
1251
1616
|
if (req.method === 'POST') {
|
|
@@ -1259,20 +1624,20 @@ async function handleHttpRequest(req, res) {
|
|
|
1259
1624
|
const parsed = JSON.parse(body);
|
|
1260
1625
|
if (Array.isArray(parsed)) {
|
|
1261
1626
|
const responses = (await Promise.all(parsed.map(m => handleJsonRpcMessage(m)))).filter(Boolean);
|
|
1262
|
-
res.writeHead(200,
|
|
1627
|
+
res.writeHead(200, corsHeaders);
|
|
1263
1628
|
res.end(JSON.stringify(responses));
|
|
1264
1629
|
} else {
|
|
1265
1630
|
const response = await handleJsonRpcMessage(parsed);
|
|
1266
1631
|
if (!response) {
|
|
1267
|
-
res.writeHead(202);
|
|
1632
|
+
res.writeHead(202, corsHeaders);
|
|
1268
1633
|
res.end();
|
|
1269
1634
|
} else {
|
|
1270
|
-
res.writeHead(200,
|
|
1635
|
+
res.writeHead(200, corsHeaders);
|
|
1271
1636
|
res.end(JSON.stringify(response));
|
|
1272
1637
|
}
|
|
1273
1638
|
}
|
|
1274
1639
|
} catch (err) {
|
|
1275
|
-
res.writeHead(400,
|
|
1640
|
+
res.writeHead(400, corsHeaders);
|
|
1276
1641
|
res.end(JSON.stringify({
|
|
1277
1642
|
jsonrpc: '2.0',
|
|
1278
1643
|
id: null,
|
|
@@ -1284,7 +1649,7 @@ async function handleHttpRequest(req, res) {
|
|
|
1284
1649
|
}
|
|
1285
1650
|
|
|
1286
1651
|
if (req.method === 'GET') {
|
|
1287
|
-
res.writeHead(200,
|
|
1652
|
+
res.writeHead(200, corsHeaders);
|
|
1288
1653
|
res.end(JSON.stringify({
|
|
1289
1654
|
name: SERVER_INFO.name,
|
|
1290
1655
|
version: SERVER_INFO.version,
|
|
@@ -1296,7 +1661,7 @@ async function handleHttpRequest(req, res) {
|
|
|
1296
1661
|
}
|
|
1297
1662
|
}
|
|
1298
1663
|
|
|
1299
|
-
res.writeHead(404,
|
|
1664
|
+
res.writeHead(404, corsHeaders);
|
|
1300
1665
|
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
1301
1666
|
}
|
|
1302
1667
|
|