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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthux-mcp",
3
- "version": "2.1.0",
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.0'
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 gemma4/qwen3.8/deepseek-r1 or Cloud Gemini/OpenAI/Claude) and test provider connectivity.',
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, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json' });
1635
+ res.writeHead(200, corsHeaders);
1271
1636
  res.end(JSON.stringify(response));
1272
1637
  }
1273
1638
  }
1274
1639
  } catch (err) {
1275
- res.writeHead(400, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json' });
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, { 'Content-Type': 'application/json' });
1664
+ res.writeHead(404, corsHeaders);
1300
1665
  res.end(JSON.stringify({ error: 'Not Found' }));
1301
1666
  }
1302
1667