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/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.
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
3529
|
+
res.writeHead(200, corsHeaders);
|
|
2942
3530
|
res.end(JSON.stringify(response));
|
|
2943
3531
|
}
|
|
2944
3532
|
}
|
|
2945
3533
|
} catch (err) {
|
|
2946
|
-
res.writeHead(400,
|
|
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,
|
|
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,
|
|
3556
|
+
res.writeHead(404, corsHeaders);
|
|
2969
3557
|
res.end(JSON.stringify({ error: "Not Found" }));
|
|
2970
3558
|
}
|
|
2971
3559
|
function handleWebSocketUpgrade(req, socket) {
|