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/dist/cli.js CHANGED
@@ -5,7 +5,9 @@ import http from "node:http";
5
5
  import crypto from "node:crypto";
6
6
  import readline from "node:readline";
7
7
  import fs from "node:fs";
8
+ import os from "node:os";
8
9
  import path from "node:path";
10
+ import { execFile, spawn } from "node:child_process";
9
11
  import { fileURLToPath } from "node:url";
10
12
 
11
13
  // extension/core/profiles.js
@@ -1194,6 +1196,25 @@ function renderScoreBar(score, length = 10) {
1194
1196
  }
1195
1197
 
1196
1198
  // extension/core/providers.js
1199
+ function stripMarkdownJsonFences(rawText) {
1200
+ let text = String(rawText || "").trim();
1201
+ if (text.startsWith("```json")) {
1202
+ text = text.slice(7);
1203
+ } else if (text.startsWith("```")) {
1204
+ text = text.slice(3);
1205
+ }
1206
+ if (text.endsWith("```")) {
1207
+ text = text.slice(0, -3);
1208
+ }
1209
+ return text.trim();
1210
+ }
1211
+ function getChromeLanguageModelApi() {
1212
+ if (typeof globalThis !== "undefined") {
1213
+ if (globalThis.LanguageModel) return { api: globalThis.LanguageModel, type: "standard" };
1214
+ if (globalThis.ai?.languageModel) return { api: globalThis.ai.languageModel, type: "legacy" };
1215
+ }
1216
+ return null;
1217
+ }
1197
1218
  var PROVIDERS = {
1198
1219
  ollama: {
1199
1220
  id: "ollama",
@@ -1620,6 +1641,222 @@ var PROVIDERS = {
1620
1641
  return false;
1621
1642
  }
1622
1643
  }
1644
+ },
1645
+ apple: {
1646
+ id: "apple",
1647
+ name: "Apple Intelligence (macOS)",
1648
+ icon: "\uF8FF",
1649
+ authType: "none",
1650
+ defaultEndpoint: "http://127.0.0.1:17483",
1651
+ models: [
1652
+ { id: "apple-on-device", name: "Apple Foundation Model (On-Device ANE)" },
1653
+ { id: "apple-pcc", name: "Apple Private Cloud Compute (macOS 27)" },
1654
+ { id: "apple-content-tagging", name: "Apple Foundation Model (Fast Tagging)" }
1655
+ ],
1656
+ modelsFetchable: true,
1657
+ buildRequest(prompt, model, options = {}) {
1658
+ const endpoint = (options.endpoint || this.defaultEndpoint).replace(/\/$/, "");
1659
+ const selectedModel = model || "apple-on-device";
1660
+ const maxChars = selectedModel === "apple-pcc" ? 28e3 : 12e3;
1661
+ const safePrompt = prompt.length > maxChars ? `${prompt.slice(0, maxChars)}
1662
+
1663
+ [DOM truncated for on-device context window. Respond ONLY with valid JSON.]` : prompt;
1664
+ return {
1665
+ url: `${endpoint}/apple-fm/generate`,
1666
+ method: "POST",
1667
+ headers: { "Content-Type": "application/json" },
1668
+ body: {
1669
+ model: selectedModel,
1670
+ prompt: safePrompt,
1671
+ system: options.systemPrompt || "You are a UX evaluation expert. Respond only with valid JSON.",
1672
+ temperature: options.temperature ?? 0.3,
1673
+ maxTokens: options.maxTokens || 2048,
1674
+ format: options.format || "json"
1675
+ }
1676
+ };
1677
+ },
1678
+ parseResponse(data) {
1679
+ if (data && data.success === false) {
1680
+ throw new Error(data.error || "Apple Foundation Model failed to generate a response.");
1681
+ }
1682
+ const rawText = stripMarkdownJsonFences(
1683
+ data?.response ?? data?.choices?.[0]?.message?.content ?? ""
1684
+ );
1685
+ try {
1686
+ const parsed = JSON.parse(rawText);
1687
+ return {
1688
+ success: true,
1689
+ result: parsed,
1690
+ meta: {
1691
+ model: data?.model || "apple-on-device",
1692
+ totalDuration: data?.totalDuration || 0,
1693
+ inputTokens: data?.inputTokens ?? data?.usage?.prompt_tokens ?? 0,
1694
+ outputTokens: data?.outputTokens ?? data?.usage?.completion_tokens ?? 0
1695
+ }
1696
+ };
1697
+ } catch {
1698
+ return {
1699
+ success: true,
1700
+ result: { raw: rawText },
1701
+ meta: {
1702
+ model: data?.model || "apple-on-device",
1703
+ inputTokens: data?.inputTokens ?? 0,
1704
+ outputTokens: data?.outputTokens ?? 0
1705
+ }
1706
+ };
1707
+ }
1708
+ },
1709
+ async fetchModels(endpoint) {
1710
+ const base = (endpoint || this.defaultEndpoint).replace(/\/$/, "");
1711
+ try {
1712
+ const res = await fetch(`${base}/apple-fm/status`, {
1713
+ signal: AbortSignal.timeout(5e3)
1714
+ });
1715
+ if (res.ok) {
1716
+ const data = await res.json();
1717
+ if (Array.isArray(data.models) && data.models.length > 0) {
1718
+ return data.models.map((m) => ({
1719
+ id: m.id || m.name,
1720
+ name: m.name || m.id
1721
+ }));
1722
+ }
1723
+ }
1724
+ } catch {
1725
+ }
1726
+ return this.models;
1727
+ },
1728
+ async ping(endpoint) {
1729
+ const base = (endpoint || this.defaultEndpoint).replace(/\/$/, "");
1730
+ try {
1731
+ const res = await fetch(`${base}/apple-fm/status`, {
1732
+ signal: AbortSignal.timeout(5e3)
1733
+ });
1734
+ if (res.ok) {
1735
+ const data = await res.json();
1736
+ if (data.available) {
1737
+ return {
1738
+ status: "connected",
1739
+ version: data.variant || data.osVersion || "Apple Intelligence"
1740
+ };
1741
+ }
1742
+ return {
1743
+ status: data.status === "unsupported_os" ? "unsupported-os" : "connected",
1744
+ version: data.variant || data.osVersion || "macOS Bridge Ready",
1745
+ warning: data.reason || null
1746
+ };
1747
+ }
1748
+ return { status: "error", code: res.status };
1749
+ } catch {
1750
+ return { status: "offline" };
1751
+ }
1752
+ }
1753
+ },
1754
+ chromeai: {
1755
+ id: "chromeai",
1756
+ name: "Chrome Built-in AI",
1757
+ icon: "\u{1F310}",
1758
+ authType: "none",
1759
+ defaultEndpoint: "chrome://on-device-internals",
1760
+ models: [
1761
+ { id: "gemini-nano", name: "Gemini Nano (Chrome Prompt API)" }
1762
+ ],
1763
+ modelsFetchable: false,
1764
+ async evaluateDirect(prompt, model = "gemini-nano", options = {}) {
1765
+ const entry = getChromeLanguageModelApi();
1766
+ if (!entry) {
1767
+ return {
1768
+ success: false,
1769
+ error: "Chrome Built-in AI (Prompt API) is not enabled in this browser. Enable chrome://flags/#prompt-api-for-gemini-nano and restart Chrome."
1770
+ };
1771
+ }
1772
+ const maxChars = 14e3;
1773
+ const safePrompt = prompt.length > maxChars ? `${prompt.slice(0, maxChars)}
1774
+
1775
+ [DOM truncated for Gemini Nano context window. Respond ONLY with valid JSON.]` : prompt;
1776
+ const systemPrompt = options.systemPrompt || "You are a UX evaluation expert. Respond ONLY with a valid JSON object without markdown code fences.";
1777
+ let session = null;
1778
+ try {
1779
+ if (entry.type === "standard") {
1780
+ session = await entry.api.create({
1781
+ initialPrompts: [{ role: "system", content: systemPrompt }],
1782
+ temperature: options.temperature ?? 0.3,
1783
+ topK: 3,
1784
+ signal: options.signal
1785
+ });
1786
+ } else {
1787
+ session = await entry.api.create({
1788
+ systemPrompt,
1789
+ temperature: options.temperature ?? 0.3,
1790
+ topK: 3,
1791
+ signal: options.signal
1792
+ });
1793
+ }
1794
+ const rawResponse = await session.prompt(safePrompt, { signal: options.signal });
1795
+ const cleaned = stripMarkdownJsonFences(rawResponse);
1796
+ const inputTokens = Math.max(1, Math.round(safePrompt.length / 4));
1797
+ const outputTokens = Math.max(1, Math.round(cleaned.length / 4));
1798
+ try {
1799
+ const parsed = JSON.parse(cleaned);
1800
+ return {
1801
+ success: true,
1802
+ result: parsed,
1803
+ meta: {
1804
+ model: model || "gemini-nano",
1805
+ inputTokens,
1806
+ outputTokens
1807
+ }
1808
+ };
1809
+ } catch {
1810
+ return {
1811
+ success: true,
1812
+ result: { raw: cleaned },
1813
+ meta: {
1814
+ model: model || "gemini-nano",
1815
+ inputTokens,
1816
+ outputTokens
1817
+ }
1818
+ };
1819
+ }
1820
+ } catch (err) {
1821
+ return {
1822
+ success: false,
1823
+ error: `Chrome Built-in AI error: ${err.message || String(err)}`
1824
+ };
1825
+ } finally {
1826
+ try {
1827
+ session?.destroy?.();
1828
+ } catch {
1829
+ }
1830
+ }
1831
+ },
1832
+ async fetchModels() {
1833
+ return this.models;
1834
+ },
1835
+ async ping() {
1836
+ const entry = getChromeLanguageModelApi();
1837
+ if (!entry) {
1838
+ return { status: "chromeai-disabled" };
1839
+ }
1840
+ try {
1841
+ if (entry.type === "standard" && typeof entry.api.availability === "function") {
1842
+ const avail = await entry.api.availability();
1843
+ if (avail === "unavailable" || avail === "no") {
1844
+ return { status: "chromeai-disabled" };
1845
+ }
1846
+ return { status: "connected", version: `Gemini Nano (${avail})` };
1847
+ }
1848
+ if (entry.type === "legacy" && typeof entry.api.capabilities === "function") {
1849
+ const caps = await entry.api.capabilities();
1850
+ if (caps?.available === "no") {
1851
+ return { status: "chromeai-disabled" };
1852
+ }
1853
+ return { status: "connected", version: `Gemini Nano (${caps?.available || "ready"})` };
1854
+ }
1855
+ return { status: "connected", version: "Gemini Nano" };
1856
+ } catch {
1857
+ return { status: "chromeai-disabled" };
1858
+ }
1859
+ }
1623
1860
  }
1624
1861
  };
1625
1862
  function getProvider(id) {
@@ -1682,6 +1919,23 @@ var AIClient = class {
1682
1919
  console.log(`[synthux] Retrying AI call (attempt ${attempt + 1})...`);
1683
1920
  await new Promise((r) => setTimeout(r, 3e3));
1684
1921
  }
1922
+ if (typeof this.provider.evaluateDirect === "function") {
1923
+ const directResult = await this.provider.evaluateDirect(prompt, model, {
1924
+ endpoint: this.endpoint,
1925
+ apiKey: this.apiKey,
1926
+ systemPrompt,
1927
+ temperature,
1928
+ format,
1929
+ maxTokens,
1930
+ images: images.filter(Boolean),
1931
+ signal: this.abortController.signal
1932
+ });
1933
+ if (!directResult.success && attempt < maxRetries) {
1934
+ lastError = new Error(directResult.error || "Direct evaluation failed");
1935
+ continue;
1936
+ }
1937
+ return directResult;
1938
+ }
1685
1939
  const req = this.provider.buildRequest(prompt, model, {
1686
1940
  endpoint: this.endpoint,
1687
1941
  apiKey: this.apiKey,
@@ -1841,7 +2095,7 @@ var PORT = Number(process.env.SYNTHUX_MCP_PORT || 17483);
1841
2095
  var REQUIRED_TOKEN = (process.env.SYNTHUX_MCP_TOKEN || "").trim();
1842
2096
  var SERVER_INFO = {
1843
2097
  name: "synthux-mcp",
1844
- version: "2.1.0"
2098
+ version: "2.1.1"
1845
2099
  };
1846
2100
  var PROTOCOL_VERSION = "2024-11-05";
1847
2101
  var localState = {
@@ -1887,6 +2141,187 @@ function validateSafeHttpUrl(urlStr) {
1887
2141
  return false;
1888
2142
  }
1889
2143
  }
2144
+ var APPLE_FM_SWIFT_PATH = [
2145
+ path.join(__dirname, "apple-fm-bridge.swift"),
2146
+ path.join(__dirname, "..", "apple-fm-bridge.swift"),
2147
+ path.join(ROOT_DIR, "mcp", "apple-fm-bridge.swift")
2148
+ ].find((p) => fs.existsSync(p)) || path.join(__dirname, "apple-fm-bridge.swift");
2149
+ var APPLE_FM_CACHE_DIR = path.join(os.tmpdir(), "synthux-apple-fm-cache");
2150
+ var APPLE_FM_BIN_PATH = path.join(APPLE_FM_CACHE_DIR, "synthux-apple-fm");
2151
+ var _appleFmCompiling = null;
2152
+ async function ensureAppleFmBinary() {
2153
+ if (process.platform !== "darwin") return null;
2154
+ if (!fs.existsSync(APPLE_FM_SWIFT_PATH)) return null;
2155
+ try {
2156
+ fs.mkdirSync(APPLE_FM_CACHE_DIR, { recursive: true });
2157
+ if (fs.existsSync(APPLE_FM_BIN_PATH)) {
2158
+ const srcStat = fs.statSync(APPLE_FM_SWIFT_PATH);
2159
+ const binStat = fs.statSync(APPLE_FM_BIN_PATH);
2160
+ if (binStat.mtimeMs >= srcStat.mtimeMs) {
2161
+ return APPLE_FM_BIN_PATH;
2162
+ }
2163
+ }
2164
+ } catch {
2165
+ }
2166
+ if (_appleFmCompiling) return _appleFmCompiling;
2167
+ _appleFmCompiling = new Promise((resolve) => {
2168
+ const moduleCache = path.join(APPLE_FM_CACHE_DIR, "swift-module-cache");
2169
+ try {
2170
+ fs.mkdirSync(moduleCache, { recursive: true });
2171
+ } catch {
2172
+ }
2173
+ execFile(
2174
+ "swiftc",
2175
+ ["-module-cache-path", moduleCache, "-O", APPLE_FM_SWIFT_PATH, "-o", APPLE_FM_BIN_PATH],
2176
+ { timeout: 3e4 },
2177
+ (err) => {
2178
+ _appleFmCompiling = null;
2179
+ if (err) {
2180
+ resolve(null);
2181
+ } else {
2182
+ try {
2183
+ fs.chmodSync(APPLE_FM_BIN_PATH, 493);
2184
+ } catch {
2185
+ }
2186
+ resolve(APPLE_FM_BIN_PATH);
2187
+ }
2188
+ }
2189
+ );
2190
+ });
2191
+ return _appleFmCompiling;
2192
+ }
2193
+ async function runAppleFmStatus() {
2194
+ if (process.platform !== "darwin") {
2195
+ return {
2196
+ available: false,
2197
+ status: "unsupported_os",
2198
+ reason: "Apple Foundation Models require macOS 26/27 on Apple Silicon.",
2199
+ osVersion: `${process.platform} (${os.release()})`,
2200
+ models: []
2201
+ };
2202
+ }
2203
+ const bin = await ensureAppleFmBinary();
2204
+ if (!bin) {
2205
+ return {
2206
+ available: false,
2207
+ status: "compiler_unavailable",
2208
+ reason: "Swift / Xcode Command Line Tools are required to bridge Apple FoundationModels on macOS.",
2209
+ osVersion: `macOS (${os.release()})`,
2210
+ models: [
2211
+ { id: "apple-on-device", name: "Apple Foundation Model (On-Device ANE)", size: 0 },
2212
+ { id: "apple-pcc", name: "Apple Private Cloud Compute (macOS 27)", size: 0 },
2213
+ { id: "apple-content-tagging", name: "Apple Foundation Model (Fast Tagging)", size: 0 }
2214
+ ]
2215
+ };
2216
+ }
2217
+ return new Promise((resolve) => {
2218
+ execFile(bin, ["status"], { timeout: 1e4 }, (err, stdout) => {
2219
+ if (err || !stdout) {
2220
+ resolve({
2221
+ available: false,
2222
+ status: "error",
2223
+ reason: err?.message || "Failed to query Apple Foundation Models status.",
2224
+ osVersion: `macOS (${os.release()})`,
2225
+ models: [
2226
+ { id: "apple-on-device", name: "Apple Foundation Model (On-Device ANE)", size: 0 },
2227
+ { id: "apple-pcc", name: "Apple Private Cloud Compute (macOS 27)", size: 0 },
2228
+ { id: "apple-content-tagging", name: "Apple Foundation Model (Fast Tagging)", size: 0 }
2229
+ ]
2230
+ });
2231
+ return;
2232
+ }
2233
+ try {
2234
+ resolve(JSON.parse(stdout.trim()));
2235
+ } catch {
2236
+ resolve({
2237
+ available: false,
2238
+ status: "parse_error",
2239
+ raw: stdout.trim(),
2240
+ models: []
2241
+ });
2242
+ }
2243
+ });
2244
+ });
2245
+ }
2246
+ async function runAppleFmGenerate(payload) {
2247
+ if (process.platform !== "darwin") {
2248
+ return {
2249
+ success: false,
2250
+ error: "Apple Foundation Models require macOS 26/27 on Apple Silicon."
2251
+ };
2252
+ }
2253
+ const bin = await ensureAppleFmBinary();
2254
+ if (!bin) {
2255
+ return {
2256
+ success: false,
2257
+ error: "Unable to compile Apple FoundationModels Swift bridge. Ensure Xcode / Command Line Tools are installed."
2258
+ };
2259
+ }
2260
+ return new Promise((resolve) => {
2261
+ const child = spawn(bin, ["generate"], {
2262
+ stdio: ["pipe", "pipe", "pipe"]
2263
+ });
2264
+ let stdout = "";
2265
+ let stderr = "";
2266
+ const timer = setTimeout(() => {
2267
+ try {
2268
+ child.kill("SIGKILL");
2269
+ } catch {
2270
+ }
2271
+ resolve({
2272
+ success: false,
2273
+ error: "Apple Foundation Model request timed out."
2274
+ });
2275
+ }, 6e4);
2276
+ child.stdout.on("data", (chunk) => {
2277
+ stdout += chunk.toString("utf8");
2278
+ });
2279
+ child.stderr.on("data", (chunk) => {
2280
+ stderr += chunk.toString("utf8");
2281
+ });
2282
+ child.on("close", () => {
2283
+ clearTimeout(timer);
2284
+ try {
2285
+ const parsed = JSON.parse(stdout.trim());
2286
+ resolve(parsed);
2287
+ } catch {
2288
+ resolve({
2289
+ success: false,
2290
+ error: stderr.trim() || stdout.trim() || "Apple Foundation Model returned an invalid response."
2291
+ });
2292
+ }
2293
+ });
2294
+ child.on("error", (err) => {
2295
+ clearTimeout(timer);
2296
+ resolve({
2297
+ success: false,
2298
+ error: `Apple Foundation Model process error: ${err.message}`
2299
+ });
2300
+ });
2301
+ child.stdin.write(JSON.stringify(payload));
2302
+ child.stdin.end();
2303
+ });
2304
+ }
2305
+ function readJsonBody(req, maxBytes = 10 * 1024 * 1024) {
2306
+ return new Promise((resolve, reject) => {
2307
+ let body = "";
2308
+ req.on("data", (chunk) => {
2309
+ body += chunk.toString("utf8");
2310
+ if (body.length > maxBytes) {
2311
+ req.destroy?.();
2312
+ reject(new Error("Payload too large"));
2313
+ }
2314
+ });
2315
+ req.on("end", () => {
2316
+ try {
2317
+ resolve(body ? JSON.parse(body) : {});
2318
+ } catch (err) {
2319
+ reject(err);
2320
+ }
2321
+ });
2322
+ req.on("error", reject);
2323
+ });
2324
+ }
1890
2325
  var WebSocketConnection = class {
1891
2326
  constructor(socket) {
1892
2327
  this.socket = socket;
@@ -2088,7 +2523,7 @@ var MCP_TOOLS = [
2088
2523
  },
2089
2524
  provider: {
2090
2525
  type: "string",
2091
- enum: ["ollama", "gemini", "openai", "claude"],
2526
+ enum: ["ollama", "apple", "chromeai", "gemini", "openai", "claude"],
2092
2527
  description: "Optional AI provider override"
2093
2528
  },
2094
2529
  model: {
@@ -2296,7 +2731,7 @@ var MCP_TOOLS = [
2296
2731
  },
2297
2732
  {
2298
2733
  name: "synthux_configure_ai",
2299
- description: "Read or update synthux AI provider configuration (Local Ollama gemma4/qwen3.8/deepseek-r1 or Cloud Gemini/OpenAI/Claude) and test provider connectivity.",
2734
+ 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.",
2300
2735
  inputSchema: {
2301
2736
  type: "object",
2302
2737
  properties: {
@@ -2305,7 +2740,7 @@ var MCP_TOOLS = [
2305
2740
  enum: ["get", "update", "ping"],
2306
2741
  default: "get"
2307
2742
  },
2308
- provider: { type: "string", enum: ["ollama", "gemini", "openai", "claude"] },
2743
+ provider: { type: "string", enum: ["ollama", "apple", "chromeai", "gemini", "openai", "claude"] },
2309
2744
  model: { type: "string" },
2310
2745
  endpoint: { type: "string" },
2311
2746
  enableVision: { type: "boolean" },
@@ -2900,17 +3335,28 @@ async function handleHttpRequest(req, res) {
2900
3335
  res.end(JSON.stringify({ error: "Forbidden: Web origins are blocked by synthux security policy." }));
2901
3336
  return;
2902
3337
  }
3338
+ const corsHeaders = {
3339
+ "Access-Control-Allow-Origin": origin || "*",
3340
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
3341
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, x-synthux-token",
3342
+ "Content-Type": "application/json"
3343
+ };
3344
+ if (req.method === "OPTIONS") {
3345
+ res.writeHead(204, corsHeaders);
3346
+ res.end();
3347
+ return;
3348
+ }
2903
3349
  if (REQUIRED_TOKEN) {
2904
3350
  const headerToken = req.headers["x-synthux-token"] || (req.headers.authorization || "").replace(/^Bearer\s+/i, "");
2905
3351
  if (headerToken !== REQUIRED_TOKEN) {
2906
- res.writeHead(401, { "Content-Type": "application/json" });
3352
+ res.writeHead(401, corsHeaders);
2907
3353
  res.end(JSON.stringify({ error: "Unauthorized: Invalid SYNTHUX_MCP_TOKEN." }));
2908
3354
  return;
2909
3355
  }
2910
3356
  }
2911
3357
  const url = new URL(req.url || "/", `http://${LOOPBACK_HOST}:${PORT}`);
2912
3358
  if (req.method === "GET" && url.pathname === "/health") {
2913
- res.writeHead(200, { "Content-Type": "application/json" });
3359
+ res.writeHead(200, corsHeaders);
2914
3360
  res.end(JSON.stringify({
2915
3361
  status: "ok",
2916
3362
  server: SERVER_INFO,
@@ -2918,6 +3364,148 @@ async function handleHttpRequest(req, res) {
2918
3364
  }));
2919
3365
  return;
2920
3366
  }
3367
+ if (req.method === "GET" && url.pathname === "/apple-fm/status") {
3368
+ const status = await runAppleFmStatus();
3369
+ res.writeHead(200, corsHeaders);
3370
+ res.end(JSON.stringify(status));
3371
+ return;
3372
+ }
3373
+ if (req.method === "POST" && url.pathname === "/apple-fm/generate") {
3374
+ try {
3375
+ const payload = await readJsonBody(req);
3376
+ if (!payload.prompt) {
3377
+ res.writeHead(400, corsHeaders);
3378
+ res.end(JSON.stringify({ success: false, error: "prompt is required" }));
3379
+ return;
3380
+ }
3381
+ const result = await runAppleFmGenerate(payload);
3382
+ res.writeHead(result.success ? 200 : 500, corsHeaders);
3383
+ res.end(JSON.stringify(result));
3384
+ } catch (err) {
3385
+ res.writeHead(400, corsHeaders);
3386
+ res.end(JSON.stringify({ success: false, error: err.message }));
3387
+ }
3388
+ return;
3389
+ }
3390
+ if (req.method === "GET" && url.pathname === "/api/tags") {
3391
+ const status = await runAppleFmStatus();
3392
+ res.writeHead(200, corsHeaders);
3393
+ res.end(JSON.stringify({
3394
+ models: (status.models || []).map((m) => ({
3395
+ name: m.id,
3396
+ model: m.id,
3397
+ size: m.size || 0,
3398
+ details: { family: "apple-foundation-model", parameter_size: "3B-ANE" }
3399
+ }))
3400
+ }));
3401
+ return;
3402
+ }
3403
+ if (req.method === "GET" && url.pathname === "/api/version") {
3404
+ const status = await runAppleFmStatus();
3405
+ res.writeHead(200, corsHeaders);
3406
+ res.end(JSON.stringify({ version: status.variant || status.osVersion || "apple-fm-27.0" }));
3407
+ return;
3408
+ }
3409
+ if (req.method === "POST" && url.pathname === "/api/show") {
3410
+ res.writeHead(200, corsHeaders);
3411
+ res.end(JSON.stringify({
3412
+ capabilities: ["completion"],
3413
+ details: { family: "apple-foundation-model" }
3414
+ }));
3415
+ return;
3416
+ }
3417
+ if (req.method === "POST" && url.pathname === "/api/generate") {
3418
+ try {
3419
+ const body = await readJsonBody(req);
3420
+ if (!body.model) {
3421
+ res.writeHead(400, corsHeaders);
3422
+ res.end(JSON.stringify({ error: "model is required" }));
3423
+ return;
3424
+ }
3425
+ const result = await runAppleFmGenerate({
3426
+ model: body.model,
3427
+ prompt: body.prompt || "",
3428
+ system: body.system || "",
3429
+ temperature: body.options?.temperature ?? 0.3,
3430
+ maxTokens: body.options?.num_predict ?? 2048
3431
+ });
3432
+ if (!result.success) {
3433
+ res.writeHead(500, corsHeaders);
3434
+ res.end(JSON.stringify({ error: result.error || "Apple Foundation Model error" }));
3435
+ return;
3436
+ }
3437
+ res.writeHead(200, corsHeaders);
3438
+ res.end(JSON.stringify({
3439
+ model: result.model || body.model,
3440
+ response: result.response,
3441
+ done: true,
3442
+ total_duration: result.totalDuration || 0,
3443
+ prompt_eval_count: result.inputTokens || 0,
3444
+ eval_count: result.outputTokens || 0
3445
+ }));
3446
+ } catch (err) {
3447
+ res.writeHead(400, corsHeaders);
3448
+ res.end(JSON.stringify({ error: err.message }));
3449
+ }
3450
+ return;
3451
+ }
3452
+ if (req.method === "GET" && url.pathname === "/v1/models") {
3453
+ const status = await runAppleFmStatus();
3454
+ res.writeHead(200, corsHeaders);
3455
+ res.end(JSON.stringify({
3456
+ object: "list",
3457
+ data: (status.models || []).map((m) => ({
3458
+ id: m.id,
3459
+ object: "model",
3460
+ owned_by: "apple-foundation-models"
3461
+ }))
3462
+ }));
3463
+ return;
3464
+ }
3465
+ if (req.method === "POST" && url.pathname === "/v1/chat/completions") {
3466
+ try {
3467
+ const body = await readJsonBody(req);
3468
+ const messages = Array.isArray(body.messages) ? body.messages : [];
3469
+ const sysMsg = messages.find((m) => m.role === "system");
3470
+ const userMsg = [...messages].reverse().find((m) => m.role === "user");
3471
+ const userText = Array.isArray(userMsg?.content) ? userMsg.content.filter((p) => p.type === "text").map((p) => p.text).join("\n") : String(userMsg?.content || "");
3472
+ const result = await runAppleFmGenerate({
3473
+ model: body.model || "apple-on-device",
3474
+ prompt: userText,
3475
+ system: sysMsg?.content || "",
3476
+ temperature: body.temperature ?? 0.3,
3477
+ maxTokens: body.max_tokens ?? 2048
3478
+ });
3479
+ if (!result.success) {
3480
+ res.writeHead(500, corsHeaders);
3481
+ res.end(JSON.stringify({ error: { message: result.error } }));
3482
+ return;
3483
+ }
3484
+ res.writeHead(200, corsHeaders);
3485
+ res.end(JSON.stringify({
3486
+ id: `chatcmpl-${Date.now()}`,
3487
+ object: "chat.completion",
3488
+ created: Math.floor(Date.now() / 1e3),
3489
+ model: result.model || body.model || "apple-on-device",
3490
+ choices: [
3491
+ {
3492
+ index: 0,
3493
+ message: { role: "assistant", content: result.response },
3494
+ finish_reason: "stop"
3495
+ }
3496
+ ],
3497
+ usage: {
3498
+ prompt_tokens: result.inputTokens || 0,
3499
+ completion_tokens: result.outputTokens || 0,
3500
+ total_tokens: (result.inputTokens || 0) + (result.outputTokens || 0)
3501
+ }
3502
+ }));
3503
+ } catch (err) {
3504
+ res.writeHead(400, corsHeaders);
3505
+ res.end(JSON.stringify({ error: { message: err.message } }));
3506
+ }
3507
+ return;
3508
+ }
2921
3509
  if (url.pathname === "/mcp") {
2922
3510
  if (req.method === "POST") {
2923
3511
  let body = "";
@@ -2930,20 +3518,20 @@ async function handleHttpRequest(req, res) {
2930
3518
  const parsed = JSON.parse(body);
2931
3519
  if (Array.isArray(parsed)) {
2932
3520
  const responses = (await Promise.all(parsed.map((m) => handleJsonRpcMessage(m)))).filter(Boolean);
2933
- res.writeHead(200, { "Content-Type": "application/json" });
3521
+ res.writeHead(200, corsHeaders);
2934
3522
  res.end(JSON.stringify(responses));
2935
3523
  } else {
2936
3524
  const response = await handleJsonRpcMessage(parsed);
2937
3525
  if (!response) {
2938
- res.writeHead(202);
3526
+ res.writeHead(202, corsHeaders);
2939
3527
  res.end();
2940
3528
  } else {
2941
- res.writeHead(200, { "Content-Type": "application/json" });
3529
+ res.writeHead(200, corsHeaders);
2942
3530
  res.end(JSON.stringify(response));
2943
3531
  }
2944
3532
  }
2945
3533
  } catch (err) {
2946
- res.writeHead(400, { "Content-Type": "application/json" });
3534
+ res.writeHead(400, corsHeaders);
2947
3535
  res.end(JSON.stringify({
2948
3536
  jsonrpc: "2.0",
2949
3537
  id: null,
@@ -2954,7 +3542,7 @@ async function handleHttpRequest(req, res) {
2954
3542
  return;
2955
3543
  }
2956
3544
  if (req.method === "GET") {
2957
- res.writeHead(200, { "Content-Type": "application/json" });
3545
+ res.writeHead(200, corsHeaders);
2958
3546
  res.end(JSON.stringify({
2959
3547
  name: SERVER_INFO.name,
2960
3548
  version: SERVER_INFO.version,
@@ -2965,7 +3553,7 @@ async function handleHttpRequest(req, res) {
2965
3553
  return;
2966
3554
  }
2967
3555
  }
2968
- res.writeHead(404, { "Content-Type": "application/json" });
3556
+ res.writeHead(404, corsHeaders);
2969
3557
  res.end(JSON.stringify({ error: "Not Found" }));
2970
3558
  }
2971
3559
  function handleWebSocketUpgrade(req, socket) {