clef-mcp 0.1.6 → 0.2.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 +168 -143
- package/assets/demo.svg +70 -0
- package/dist/cli.js +1009 -290
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +556 -62
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ function limitsFromEnv(env = process.env) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// src/config/env.ts
|
|
32
|
+
var SUPPORTED_RUNTIMES = ["llama-cpp", "mlx"];
|
|
32
33
|
var SUPPORTED_LOG_LEVELS = ["error", "warn", "info", "debug"];
|
|
33
34
|
function parseLogLevel(value) {
|
|
34
35
|
if (!value) return void 0;
|
|
@@ -40,19 +41,20 @@ function defaultClefHome() {
|
|
|
40
41
|
}
|
|
41
42
|
function loadConfig(env = process.env) {
|
|
42
43
|
const logLevel = parseLogLevel(env.CLEF_LOG_LEVEL) ?? "error";
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
throw new Error(`Unsupported CLEF_RUNTIME "${
|
|
44
|
+
const runtimeRaw = (env.CLEF_RUNTIME?.trim() || "llama-cpp").toLowerCase();
|
|
45
|
+
if (!SUPPORTED_RUNTIMES.includes(runtimeRaw)) {
|
|
46
|
+
throw new Error(`Unsupported CLEF_RUNTIME "${runtimeRaw}". Supported: ${SUPPORTED_RUNTIMES.join(", ")}`);
|
|
46
47
|
}
|
|
48
|
+
const runtime = runtimeRaw;
|
|
47
49
|
const llamaBin = env.CLEF_LLAMA_BIN?.trim() || void 0;
|
|
48
50
|
const llamaReleaseTag = env.CLEF_LLAMA_RELEASE_TAG?.trim() || void 0;
|
|
49
51
|
return {
|
|
50
52
|
model: env.CLEF_MODEL?.trim() || "clef-flash",
|
|
51
53
|
clefHome: env.CLEF_HOME?.trim() || defaultClefHome(),
|
|
52
|
-
runtime
|
|
54
|
+
runtime,
|
|
53
55
|
logLevel,
|
|
54
|
-
llamaBin,
|
|
55
|
-
llamaReleaseTag,
|
|
56
|
+
llamaBin: runtime === "llama-cpp" ? llamaBin : void 0,
|
|
57
|
+
llamaReleaseTag: runtime === "llama-cpp" ? llamaReleaseTag : void 0,
|
|
56
58
|
limits: limitsFromEnv(env)
|
|
57
59
|
};
|
|
58
60
|
}
|
|
@@ -664,10 +666,10 @@ function isModelManifest(v) {
|
|
|
664
666
|
const m = v;
|
|
665
667
|
return m.schemaVersion === 1 && typeof m.model === "string" && typeof m.quant === "string" && typeof m.file === "string" && typeof m.modelPath === "string" && typeof m.bytes === "number";
|
|
666
668
|
}
|
|
667
|
-
async function readModelManifest(
|
|
669
|
+
async function readModelManifest(manifestPath2) {
|
|
668
670
|
let raw;
|
|
669
671
|
try {
|
|
670
|
-
raw = await fsp.readFile(
|
|
672
|
+
raw = await fsp.readFile(manifestPath2, "utf8");
|
|
671
673
|
} catch {
|
|
672
674
|
return void 0;
|
|
673
675
|
}
|
|
@@ -678,9 +680,9 @@ async function readModelManifest(manifestPath) {
|
|
|
678
680
|
return void 0;
|
|
679
681
|
}
|
|
680
682
|
}
|
|
681
|
-
async function writeModelManifest(
|
|
682
|
-
await fsp.mkdir(path3.dirname(
|
|
683
|
-
await fsp.writeFile(
|
|
683
|
+
async function writeModelManifest(manifestPath2, manifest) {
|
|
684
|
+
await fsp.mkdir(path3.dirname(manifestPath2), { recursive: true });
|
|
685
|
+
await fsp.writeFile(manifestPath2, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
684
686
|
}
|
|
685
687
|
async function findInstalledQuant(modelsDir, model, specs) {
|
|
686
688
|
const installed = [];
|
|
@@ -733,13 +735,13 @@ function isRuntimeManifest(v) {
|
|
|
733
735
|
const m = v;
|
|
734
736
|
return m.schemaVersion === 1 && m.kind === "llama-cpp" && typeof m.releaseTag === "string" && typeof m.asset === "string" && typeof m.binaryPath === "string";
|
|
735
737
|
}
|
|
736
|
-
async function writeRuntimeManifest(
|
|
737
|
-
await fsp.mkdir(path3.dirname(
|
|
738
|
-
await fsp.writeFile(
|
|
738
|
+
async function writeRuntimeManifest(manifestPath2, manifest) {
|
|
739
|
+
await fsp.mkdir(path3.dirname(manifestPath2), { recursive: true });
|
|
740
|
+
await fsp.writeFile(manifestPath2, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
739
741
|
}
|
|
740
|
-
function readRuntimeManifest(
|
|
742
|
+
function readRuntimeManifest(manifestPath2) {
|
|
741
743
|
try {
|
|
742
|
-
const parsed = JSON.parse(fs.readFileSync(
|
|
744
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath2, "utf8"));
|
|
743
745
|
return isRuntimeManifest(parsed) ? parsed : void 0;
|
|
744
746
|
} catch {
|
|
745
747
|
return void 0;
|
|
@@ -801,6 +803,259 @@ function resolveLlamaServerBinary(cfg, deps = {}) {
|
|
|
801
803
|
|
|
802
804
|
// src/runtime/probe.ts
|
|
803
805
|
import fs3 from "fs/promises";
|
|
806
|
+
|
|
807
|
+
// src/models/mlx-install.ts
|
|
808
|
+
import { execFile } from "child_process";
|
|
809
|
+
import fsSync from "fs";
|
|
810
|
+
import fsp2 from "fs/promises";
|
|
811
|
+
import path5 from "path";
|
|
812
|
+
function manifestPath(clefHome) {
|
|
813
|
+
return path5.join(pathsFor(clefHome).modelsDir, "clef-flash", "mlx", "manifest.json");
|
|
814
|
+
}
|
|
815
|
+
async function readMlxManifest(clefHome) {
|
|
816
|
+
let raw;
|
|
817
|
+
try {
|
|
818
|
+
raw = await fsp2.readFile(manifestPath(clefHome), "utf8");
|
|
819
|
+
} catch {
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const parsed = JSON.parse(raw);
|
|
824
|
+
if (parsed.kind === "mlx" && typeof parsed.snapshotPath === "string") return parsed;
|
|
825
|
+
return void 0;
|
|
826
|
+
} catch {
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function mlxIsInstalled(clefHome) {
|
|
831
|
+
const manifest = await readMlxManifest(clefHome);
|
|
832
|
+
if (!manifest) return void 0;
|
|
833
|
+
try {
|
|
834
|
+
await fsp2.access(path5.join(manifest.snapshotPath, "clef_mlx.py"));
|
|
835
|
+
return manifest;
|
|
836
|
+
} catch {
|
|
837
|
+
return void 0;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function resolveUv(envUv) {
|
|
841
|
+
if (envUv?.trim()) return envUv.trim();
|
|
842
|
+
const dirs = (process.env.PATH ?? "").split(path5.delimiter).filter(Boolean);
|
|
843
|
+
for (const dir of dirs) {
|
|
844
|
+
const candidate = path5.join(dir, "uv");
|
|
845
|
+
try {
|
|
846
|
+
fsSync.accessSync(candidate, fsSync.constants.X_OK);
|
|
847
|
+
return candidate;
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
throw new ClefError(
|
|
852
|
+
ClefErrorCode.RUNTIME_NOT_FOUND,
|
|
853
|
+
"uv is required for the MLX runtime but was not found on PATH.",
|
|
854
|
+
"Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/runtime/mlx.ts
|
|
859
|
+
import { spawn as spawn2 } from "child_process";
|
|
860
|
+
import net2 from "net";
|
|
861
|
+
var STDERR_TAIL_LIMIT2 = 4e3;
|
|
862
|
+
function freePort2() {
|
|
863
|
+
return new Promise((resolve, reject) => {
|
|
864
|
+
const srv = net2.createServer();
|
|
865
|
+
srv.unref();
|
|
866
|
+
srv.on("error", reject);
|
|
867
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
868
|
+
const addr = srv.address();
|
|
869
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
870
|
+
srv.close(() => port > 0 ? resolve(port) : reject(new Error("could not acquire a free port")));
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
async function fetchWithTimeout2(url, init, timeoutMs) {
|
|
875
|
+
const controller = new AbortController();
|
|
876
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
877
|
+
try {
|
|
878
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
879
|
+
} finally {
|
|
880
|
+
clearTimeout(timer);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
var OOM_PATTERNS2 = ["out of memory", "failed to allocate", "unable to allocate", "not enough memory"];
|
|
884
|
+
var MlxRuntime = class {
|
|
885
|
+
constructor(opts) {
|
|
886
|
+
this.opts = opts;
|
|
887
|
+
this.host = opts.host ?? "127.0.0.1";
|
|
888
|
+
this.loadTimeoutMs = opts.loadTimeoutMs ?? 18e4;
|
|
889
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? 12e4;
|
|
890
|
+
}
|
|
891
|
+
opts;
|
|
892
|
+
proc;
|
|
893
|
+
resolvedPort;
|
|
894
|
+
host;
|
|
895
|
+
loadTimeoutMs;
|
|
896
|
+
requestTimeoutMs;
|
|
897
|
+
queue = Promise.resolve();
|
|
898
|
+
get endpoint() {
|
|
899
|
+
if (!this.resolvedPort) throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "Runtime is not started");
|
|
900
|
+
return `http://${this.host}:${this.resolvedPort}`;
|
|
901
|
+
}
|
|
902
|
+
get running() {
|
|
903
|
+
return this.proc !== void 0 && this.proc.exitCode === null;
|
|
904
|
+
}
|
|
905
|
+
async load() {
|
|
906
|
+
if (this.running) return;
|
|
907
|
+
const port = this.opts.port ?? await freePort2();
|
|
908
|
+
this.resolvedPort = port;
|
|
909
|
+
const args = [
|
|
910
|
+
"run",
|
|
911
|
+
"--directory",
|
|
912
|
+
this.opts.snapshotPath,
|
|
913
|
+
"--with",
|
|
914
|
+
"mlx-vlm>=0.7.4,<0.8",
|
|
915
|
+
"--with",
|
|
916
|
+
"huggingface_hub",
|
|
917
|
+
"python",
|
|
918
|
+
"clef_mlx.py",
|
|
919
|
+
"serve",
|
|
920
|
+
"--host",
|
|
921
|
+
this.host,
|
|
922
|
+
"--port",
|
|
923
|
+
String(port)
|
|
924
|
+
];
|
|
925
|
+
this.opts.log?.debug("spawning clef_mlx.py serve", { uv: this.opts.uvBin, snapshot: this.opts.snapshotPath });
|
|
926
|
+
let proc;
|
|
927
|
+
try {
|
|
928
|
+
proc = spawn2(this.opts.uvBin, args, { stdio: ["ignore", "pipe", "pipe"], cwd: this.opts.snapshotPath });
|
|
929
|
+
} catch (err) {
|
|
930
|
+
throw new ClefError(
|
|
931
|
+
ClefErrorCode.RUNTIME_INIT_FAILED,
|
|
932
|
+
`Failed to start the MLX server: ${err instanceof Error ? err.message : String(err)}`,
|
|
933
|
+
"Run `clef-mcp doctor` for a full diagnosis."
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
this.proc = proc;
|
|
937
|
+
let stderrTail = "";
|
|
938
|
+
proc.stderr?.on("data", (chunk) => {
|
|
939
|
+
const text = chunk.toString();
|
|
940
|
+
stderrTail = (stderrTail + text).slice(-STDERR_TAIL_LIMIT2);
|
|
941
|
+
this.opts.log?.debug(text.trimEnd());
|
|
942
|
+
});
|
|
943
|
+
proc.stdout?.on("data", (chunk) => {
|
|
944
|
+
this.opts.log?.debug(chunk.toString().trimEnd());
|
|
945
|
+
});
|
|
946
|
+
let rejectOnExit;
|
|
947
|
+
const exitedBeforeReady = new Promise((_resolve, reject) => {
|
|
948
|
+
rejectOnExit = reject;
|
|
949
|
+
});
|
|
950
|
+
const onEarlyExit = (code, signal) => {
|
|
951
|
+
const oom = OOM_PATTERNS2.some((p) => stderrTail.toLowerCase().includes(p));
|
|
952
|
+
const message = `clef_mlx.py exited before becoming ready (code=${code ?? "null"}, signal=${signal ?? "null"})`;
|
|
953
|
+
const tail = stderrTail.trim();
|
|
954
|
+
rejectOnExit?.(
|
|
955
|
+
oom ? new ClefError(ClefErrorCode.OUT_OF_MEMORY, message, "Not enough free memory for this model. Try the llama.cpp runtime with Q4_K_M instead.") : new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, message + (tail ? `
|
|
956
|
+
stderr tail:
|
|
957
|
+
${tail}` : ""), "Run `clef-mcp doctor` for a full diagnosis.")
|
|
958
|
+
);
|
|
959
|
+
};
|
|
960
|
+
proc.once("exit", onEarlyExit);
|
|
961
|
+
try {
|
|
962
|
+
await Promise.race([this.waitUntilReady(), exitedBeforeReady]);
|
|
963
|
+
} finally {
|
|
964
|
+
proc.removeListener("exit", onEarlyExit);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async waitUntilReady() {
|
|
968
|
+
const deadline = Date.now() + this.loadTimeoutMs;
|
|
969
|
+
let lastError = "";
|
|
970
|
+
while (Date.now() < deadline) {
|
|
971
|
+
if (!this.running) throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "MLX server is not running");
|
|
972
|
+
try {
|
|
973
|
+
const res = await fetchWithTimeout2(`${this.endpoint}/health`, { method: "GET" }, 2e3);
|
|
974
|
+
if (res.status === 200) return;
|
|
975
|
+
lastError = `health status ${res.status}`;
|
|
976
|
+
} catch (err) {
|
|
977
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
978
|
+
}
|
|
979
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
980
|
+
}
|
|
981
|
+
throw new ClefError(
|
|
982
|
+
ClefErrorCode.MODEL_LOAD_FAILED,
|
|
983
|
+
`MLX server did not become ready within ${Math.round(this.loadTimeoutMs / 1e3)}s (last health check: ${lastError})`,
|
|
984
|
+
"The first load can take a while; subsequent loads reuse the snapshot cache."
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
async decide(input) {
|
|
988
|
+
const run = this.queue.then(() => this.decideOnce(input));
|
|
989
|
+
this.queue = run.catch(() => void 0);
|
|
990
|
+
return run;
|
|
991
|
+
}
|
|
992
|
+
async decideOnce(input) {
|
|
993
|
+
if (!this.running) {
|
|
994
|
+
throw new ClefError(ClefErrorCode.RUNTIME_INIT_FAILED, "Runtime is not loaded; call load() first");
|
|
995
|
+
}
|
|
996
|
+
let res;
|
|
997
|
+
try {
|
|
998
|
+
res = await fetchWithTimeout2(
|
|
999
|
+
`${this.endpoint}/v1/systemone`,
|
|
1000
|
+
{ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(input) },
|
|
1001
|
+
this.requestTimeoutMs
|
|
1002
|
+
);
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1005
|
+
throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Inference request failed: ${msg}`);
|
|
1006
|
+
}
|
|
1007
|
+
const bodyText = await res.text();
|
|
1008
|
+
let body = void 0;
|
|
1009
|
+
try {
|
|
1010
|
+
body = bodyText ? JSON.parse(bodyText) : void 0;
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
if (!res.ok) {
|
|
1014
|
+
const apiMessage = body && typeof body === "object" && "error" in body ? String(body.error?.message ?? bodyText) : bodyText;
|
|
1015
|
+
throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Inference failed with HTTP ${res.status}: ${apiMessage.slice(0, 2e3)}`);
|
|
1016
|
+
}
|
|
1017
|
+
if (!body || typeof body !== "object") {
|
|
1018
|
+
throw new ClefError(ClefErrorCode.CLEF_INFERENCE_FAILED, `Runtime returned a non-JSON response (HTTP ${res.status})`);
|
|
1019
|
+
}
|
|
1020
|
+
return body;
|
|
1021
|
+
}
|
|
1022
|
+
async health() {
|
|
1023
|
+
if (!this.running) return { status: "unavailable", detail: "not started" };
|
|
1024
|
+
try {
|
|
1025
|
+
const res = await fetchWithTimeout2(`${this.endpoint}/health`, { method: "GET" }, 1500);
|
|
1026
|
+
if (res.status === 200) return { status: "ready" };
|
|
1027
|
+
return { status: "loading", detail: `health status ${res.status}` };
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
return { status: "unavailable", detail: err instanceof Error ? err.message : String(err) };
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async unload() {
|
|
1033
|
+
const proc = this.proc;
|
|
1034
|
+
if (!proc) return;
|
|
1035
|
+
this.proc = void 0;
|
|
1036
|
+
if (proc.exitCode !== null || proc.signalCode) return;
|
|
1037
|
+
await new Promise((resolve) => {
|
|
1038
|
+
const killTimer = setTimeout(() => {
|
|
1039
|
+
try {
|
|
1040
|
+
proc.kill("SIGKILL");
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
}, 3e3);
|
|
1044
|
+
proc.once("exit", () => {
|
|
1045
|
+
clearTimeout(killTimer);
|
|
1046
|
+
resolve();
|
|
1047
|
+
});
|
|
1048
|
+
try {
|
|
1049
|
+
proc.kill("SIGTERM");
|
|
1050
|
+
} catch {
|
|
1051
|
+
clearTimeout(killTimer);
|
|
1052
|
+
resolve();
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
// src/runtime/probe.ts
|
|
804
1059
|
async function probeInference(cfg, manifest, log, loadTimeoutMs = 12e4) {
|
|
805
1060
|
await fs3.access(manifest.modelPath).catch(() => {
|
|
806
1061
|
throw new ClefError(
|
|
@@ -835,8 +1090,8 @@ async function probeInference(cfg, manifest, log, loadTimeoutMs = 12e4) {
|
|
|
835
1090
|
|
|
836
1091
|
// src/models/hf.ts
|
|
837
1092
|
import { createHash as createHash2 } from "crypto";
|
|
838
|
-
import
|
|
839
|
-
import
|
|
1093
|
+
import fsp3 from "fs/promises";
|
|
1094
|
+
import path6 from "path";
|
|
840
1095
|
var HF_BASE_URL = "https://huggingface.co";
|
|
841
1096
|
function resolveUrl(repo, revision, file) {
|
|
842
1097
|
return `${HF_BASE_URL}/${repo}/resolve/${revision}/${file}`;
|
|
@@ -894,7 +1149,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
894
1149
|
}
|
|
895
1150
|
const total = opts.expectedSize ?? (res.headers.get("content-length") ? Number(res.headers.get("content-length")) : void 0);
|
|
896
1151
|
const hash = createHash2("sha256");
|
|
897
|
-
const handle = await
|
|
1152
|
+
const handle = await fsp3.open(dest, "w");
|
|
898
1153
|
let bytes = 0;
|
|
899
1154
|
try {
|
|
900
1155
|
for await (const chunk of res.body) {
|
|
@@ -909,7 +1164,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
909
1164
|
}
|
|
910
1165
|
const sha256 = hash.digest("hex");
|
|
911
1166
|
if (opts.expectedSha256 && sha256 !== opts.expectedSha256) {
|
|
912
|
-
await
|
|
1167
|
+
await fsp3.rm(dest, { force: true });
|
|
913
1168
|
throw new ClefError(
|
|
914
1169
|
ClefErrorCode.CHECKSUM_MISMATCH,
|
|
915
1170
|
`Checksum mismatch after download: expected ${opts.expectedSha256}, got ${sha256}`,
|
|
@@ -917,7 +1172,7 @@ async function downloadOnce(url, dest, opts) {
|
|
|
917
1172
|
);
|
|
918
1173
|
}
|
|
919
1174
|
if (opts.expectedSize !== void 0 && bytes !== opts.expectedSize) {
|
|
920
|
-
await
|
|
1175
|
+
await fsp3.rm(dest, { force: true });
|
|
921
1176
|
throw new ClefError(
|
|
922
1177
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
923
1178
|
`Downloaded size mismatch: expected ${opts.expectedSize} bytes, got ${bytes}`
|
|
@@ -928,24 +1183,24 @@ async function downloadOnce(url, dest, opts) {
|
|
|
928
1183
|
async function installModel(opts) {
|
|
929
1184
|
const { modelSpec, quantSpec } = opts;
|
|
930
1185
|
const { modelsDir } = pathsFor(opts.clefHome);
|
|
931
|
-
const
|
|
932
|
-
const existing = await readModelManifest(
|
|
1186
|
+
const manifestPath2 = manifestPathFor(modelsDir, modelSpec.id, quantSpec.id);
|
|
1187
|
+
const existing = await readModelManifest(manifestPath2);
|
|
933
1188
|
if (existing && (await verifyModelFiles(existing)).ok) {
|
|
934
1189
|
opts.log?.debug("model already installed", { model: modelSpec.id, quant: quantSpec.id });
|
|
935
1190
|
return { manifest: existing, alreadyInstalled: true };
|
|
936
1191
|
}
|
|
937
1192
|
const info = await fetchFileInfo(modelSpec.repo, modelSpec.revision, quantSpec.file);
|
|
938
|
-
const finalPath =
|
|
939
|
-
await
|
|
940
|
-
const tmpPath =
|
|
941
|
-
await
|
|
1193
|
+
const finalPath = path6.join(path6.dirname(manifestPath2), quantSpec.file);
|
|
1194
|
+
await fsp3.mkdir(path6.dirname(finalPath), { recursive: true });
|
|
1195
|
+
const tmpPath = path6.join(path6.dirname(finalPath), `.download-${quantSpec.file}`);
|
|
1196
|
+
await fsp3.rm(tmpPath, { force: true });
|
|
942
1197
|
opts.log?.info("downloading model", { repo: modelSpec.repo, file: quantSpec.file });
|
|
943
1198
|
const result = await downloadFile(info.downloadUrl, tmpPath, {
|
|
944
1199
|
expectedSha256: info.sha256,
|
|
945
1200
|
expectedSize: info.size ?? quantSpec.bytes,
|
|
946
1201
|
onProgress: opts.onProgress
|
|
947
1202
|
});
|
|
948
|
-
await
|
|
1203
|
+
await fsp3.rename(tmpPath, finalPath);
|
|
949
1204
|
const manifest = {
|
|
950
1205
|
schemaVersion: 1,
|
|
951
1206
|
model: modelSpec.id,
|
|
@@ -961,15 +1216,15 @@ async function installModel(opts) {
|
|
|
961
1216
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
962
1217
|
clefMcpVersion: opts.clefMcpVersion
|
|
963
1218
|
};
|
|
964
|
-
await writeModelManifest(
|
|
1219
|
+
await writeModelManifest(manifestPath2, manifest);
|
|
965
1220
|
opts.log?.info("model installed", { model: modelSpec.id, quant: quantSpec.id, sha256: result.sha256 });
|
|
966
1221
|
return { manifest, alreadyInstalled: false };
|
|
967
1222
|
}
|
|
968
1223
|
|
|
969
1224
|
// src/models/llama-install.ts
|
|
970
|
-
import { execFile } from "child_process";
|
|
971
|
-
import
|
|
972
|
-
import
|
|
1225
|
+
import { execFile as execFile2 } from "child_process";
|
|
1226
|
+
import fsp4 from "fs/promises";
|
|
1227
|
+
import path7 from "path";
|
|
973
1228
|
var GITHUB_API = "https://api.github.com/repos/ggml-org/llama.cpp";
|
|
974
1229
|
async function ghJson(url) {
|
|
975
1230
|
let res;
|
|
@@ -1023,7 +1278,7 @@ function pickAsset(release, platform, arch) {
|
|
|
1023
1278
|
}
|
|
1024
1279
|
function execTar(args, cwd) {
|
|
1025
1280
|
return new Promise((resolve, reject) => {
|
|
1026
|
-
|
|
1281
|
+
execFile2("tar", args, { cwd, timeout: 3e5 }, (err, _stdout, stderr) => {
|
|
1027
1282
|
if (err) reject(new Error(`tar ${args.join(" ")} failed: ${stderr || err.message}`));
|
|
1028
1283
|
else resolve();
|
|
1029
1284
|
});
|
|
@@ -1036,12 +1291,12 @@ async function findServerBinary(dir, platform) {
|
|
|
1036
1291
|
const current = queue.shift();
|
|
1037
1292
|
let entries;
|
|
1038
1293
|
try {
|
|
1039
|
-
entries = await
|
|
1294
|
+
entries = await fsp4.readdir(current, { withFileTypes: true });
|
|
1040
1295
|
} catch {
|
|
1041
1296
|
continue;
|
|
1042
1297
|
}
|
|
1043
1298
|
for (const entry of entries) {
|
|
1044
|
-
const full =
|
|
1299
|
+
const full = path7.join(current, entry.name);
|
|
1045
1300
|
if (entry.isDirectory()) queue.push(full);
|
|
1046
1301
|
else if (entry.name === target) return full;
|
|
1047
1302
|
}
|
|
@@ -1052,14 +1307,14 @@ async function findServerBinary(dir, platform) {
|
|
|
1052
1307
|
);
|
|
1053
1308
|
}
|
|
1054
1309
|
async function extractRelease(archivePath, destDir, platform) {
|
|
1055
|
-
await
|
|
1310
|
+
await fsp4.mkdir(destDir, { recursive: true });
|
|
1056
1311
|
const args = archivePath.endsWith(".zip") ? ["-xf", archivePath] : ["-xzf", archivePath];
|
|
1057
1312
|
try {
|
|
1058
1313
|
await execTar(args, destDir);
|
|
1059
1314
|
} catch (err) {
|
|
1060
1315
|
throw new ClefError(
|
|
1061
1316
|
ClefErrorCode.DOWNLOAD_FAILED,
|
|
1062
|
-
`Could not extract ${
|
|
1317
|
+
`Could not extract ${path7.basename(archivePath)}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1063
1318
|
"Extract the archive manually and point CLEF_LLAMA_BIN at llama-server."
|
|
1064
1319
|
);
|
|
1065
1320
|
}
|
|
@@ -1069,7 +1324,7 @@ async function ensureLlamaRuntime(cfg, log) {
|
|
|
1069
1324
|
const resolved = resolveLlamaServerBinary(cfg);
|
|
1070
1325
|
if (resolved.source === "managed") {
|
|
1071
1326
|
const platformDir = llamaRuntimeDir(pathsFor(cfg.clefHome).runtimeDir, process.platform, process.arch);
|
|
1072
|
-
const manifest = readRuntimeManifest(
|
|
1327
|
+
const manifest = readRuntimeManifest(path7.join(platformDir, "runtime-manifest.json"));
|
|
1073
1328
|
return { ...resolved, releaseTag: manifest?.releaseTag ?? "unknown", freshInstall: false };
|
|
1074
1329
|
}
|
|
1075
1330
|
return { ...resolved, releaseTag: resolved.source, freshInstall: false };
|
|
@@ -1101,16 +1356,16 @@ async function downloadManagedLlama(cfg, log) {
|
|
|
1101
1356
|
}
|
|
1102
1357
|
const asset = pickAsset(release, platform, arch);
|
|
1103
1358
|
log?.info("downloading llama.cpp runtime", { tag: release.tag_name, asset: asset.name });
|
|
1104
|
-
const tagDir =
|
|
1105
|
-
await
|
|
1106
|
-
const tmpArchive =
|
|
1107
|
-
await
|
|
1359
|
+
const tagDir = path7.join(platformDir, release.tag_name);
|
|
1360
|
+
await fsp4.mkdir(tagDir, { recursive: true });
|
|
1361
|
+
const tmpArchive = path7.join(platformDir, `.download-${asset.name}`);
|
|
1362
|
+
await fsp4.rm(tmpArchive, { force: true });
|
|
1108
1363
|
const result = await downloadFile(asset.browser_download_url, tmpArchive, { expectedSize: asset.size, attempts: 3 });
|
|
1109
1364
|
await extractRelease(tmpArchive, tagDir, platform);
|
|
1110
|
-
await
|
|
1365
|
+
await fsp4.rm(tmpArchive, { force: true });
|
|
1111
1366
|
const binaryPath = await findServerBinary(tagDir, platform);
|
|
1112
|
-
if (platform !== "win32") await
|
|
1113
|
-
await writeRuntimeManifest(
|
|
1367
|
+
if (platform !== "win32") await fsp4.chmod(binaryPath, 493);
|
|
1368
|
+
await writeRuntimeManifest(path7.join(platformDir, "runtime-manifest.json"), {
|
|
1114
1369
|
schemaVersion: 1,
|
|
1115
1370
|
kind: "llama-cpp",
|
|
1116
1371
|
releaseTag: release.tag_name,
|
|
@@ -1126,20 +1381,20 @@ async function removeManagedRuntime(cfg) {
|
|
|
1126
1381
|
const { runtimeDir } = pathsFor(cfg.clefHome);
|
|
1127
1382
|
const dir = llamaRuntimeDir(runtimeDir, process.platform, process.arch);
|
|
1128
1383
|
try {
|
|
1129
|
-
await
|
|
1384
|
+
await fsp4.stat(dir);
|
|
1130
1385
|
} catch {
|
|
1131
1386
|
return false;
|
|
1132
1387
|
}
|
|
1133
|
-
await
|
|
1388
|
+
await fsp4.rm(dir, { recursive: true, force: true });
|
|
1134
1389
|
return true;
|
|
1135
1390
|
}
|
|
1136
1391
|
|
|
1137
1392
|
// src/models/platform.ts
|
|
1138
|
-
import { execFile as
|
|
1393
|
+
import { execFile as execFile3 } from "child_process";
|
|
1139
1394
|
import os2 from "os";
|
|
1140
1395
|
function exec(cmd, args, timeoutMs) {
|
|
1141
1396
|
return new Promise((resolve, reject) => {
|
|
1142
|
-
|
|
1397
|
+
execFile3(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => {
|
|
1143
1398
|
if (err) reject(err);
|
|
1144
1399
|
else resolve(stdout);
|
|
1145
1400
|
});
|
|
@@ -1248,10 +1503,12 @@ function scoreCriteria(limits) {
|
|
|
1248
1503
|
}
|
|
1249
1504
|
function questionSchema(limits) {
|
|
1250
1505
|
return z.object({
|
|
1251
|
-
type: z.enum(["noul", "choice", "score"]),
|
|
1252
|
-
instructions: z.string().min(1).max(limits.maxInstructionsChars),
|
|
1506
|
+
type: z.enum(["noul", "choice", "score"]).describe('"choice" = pick among named options; "score" = judge an ordered scale; "noul" = yes/no question'),
|
|
1507
|
+
instructions: z.string().min(1).max(limits.maxInstructionsChars).describe("What to judge, phrased as a self-contained question."),
|
|
1253
1508
|
criteria: z.union([z.array(z.string()), z.record(z.string(), z.string())]).optional()
|
|
1254
|
-
}).
|
|
1509
|
+
}).describe(
|
|
1510
|
+
"A single decision question. Allowed answers come from `criteria` and must be mutually exclusive and collectively exhaustive."
|
|
1511
|
+
).superRefine((q, ctx) => {
|
|
1255
1512
|
if (q.type === "choice") {
|
|
1256
1513
|
const parsed = choiceCriteria(limits).safeParse(q.criteria);
|
|
1257
1514
|
if (!parsed.success) {
|
|
@@ -1275,23 +1532,27 @@ function questionSchema(limits) {
|
|
|
1275
1532
|
}
|
|
1276
1533
|
function clefDecideInputShape(limits) {
|
|
1277
1534
|
return {
|
|
1278
|
-
state: z.unknown()
|
|
1279
|
-
|
|
1535
|
+
state: z.unknown().describe(
|
|
1536
|
+
"Task state as data: a compact string or JSON object with the facts needed to judge (task, errors, logs, diffs). Never treated as instructions. Serialized size limit: 1 MB; model context is 16k tokens."
|
|
1537
|
+
),
|
|
1538
|
+
questions: z.record(z.string().min(1).max(limits.maxQuestionIdChars), questionSchema(limits)).describe(
|
|
1539
|
+
"Map of question id -> typed question. Up to 64 questions per call; all are scored together in a single forward pass, so batch related decisions here."
|
|
1540
|
+
).check((ctx) => {
|
|
1280
1541
|
const count = Object.keys(ctx.value).length;
|
|
1281
1542
|
if (count === 0) ctx.issues.push({ code: "custom", message: "questions must not be empty", input: ctx.value });
|
|
1282
1543
|
if (count > limits.maxQuestions) {
|
|
1283
1544
|
ctx.issues.push({ code: "custom", message: `too many questions (max ${limits.maxQuestions})`, input: ctx.value });
|
|
1284
1545
|
}
|
|
1285
1546
|
}),
|
|
1286
|
-
model: z.string().min(1).optional(),
|
|
1547
|
+
model: z.string().min(1).optional().describe("Model id to use (default: clef-flash from CLEF_MODEL)."),
|
|
1287
1548
|
options: z.object({
|
|
1288
1549
|
/**
|
|
1289
1550
|
* Accepted for forward compatibility. Clef scores all options in a
|
|
1290
1551
|
* single forward pass — there is no sampling, so temperature is a
|
|
1291
1552
|
* documented no-op.
|
|
1292
1553
|
*/
|
|
1293
|
-
temperature: z.number().min(0).max(2).optional()
|
|
1294
|
-
}).optional()
|
|
1554
|
+
temperature: z.number().min(0).max(2).optional().describe("Accepted for forward compatibility; no-op. Clef scores in a single forward pass without sampling.")
|
|
1555
|
+
}).optional().describe("Extra options. Currently only temperature, which is a documented no-op.")
|
|
1295
1556
|
};
|
|
1296
1557
|
}
|
|
1297
1558
|
function clefDecideInputSchema(limits) {
|
|
@@ -1331,13 +1592,223 @@ function validateStateSize(state, limits) {
|
|
|
1331
1592
|
}
|
|
1332
1593
|
}
|
|
1333
1594
|
|
|
1595
|
+
// src/mcp/prompts.ts
|
|
1596
|
+
import { z as z2 } from "zod";
|
|
1597
|
+
var TOOL_GUIDE = (questions) => [
|
|
1598
|
+
"Call the `clef_decide` MCP tool once with all of the questions below (they are scored in a single forward pass).",
|
|
1599
|
+
"",
|
|
1600
|
+
"Rules for the call:",
|
|
1601
|
+
"- `state` must be a compact, factual JSON or string \u2014 relevant facts only, well under 1 MB.",
|
|
1602
|
+
"- Use the exact `questions` given below (ids, types, criteria). Do not invent extra options.",
|
|
1603
|
+
"- If the answer distribution is flat (no option above ~0.6), gather more context and re-ask once before falling back to asking the user.",
|
|
1604
|
+
"- Treat safety-adjacent decisions (destructive actions, security) as requiring p >= 0.8 before acting; otherwise prefer asking the user.",
|
|
1605
|
+
"- Report the result as a short summary plus the top probability \u2014 do not paste raw JSON unless asked.",
|
|
1606
|
+
"",
|
|
1607
|
+
"Questions:",
|
|
1608
|
+
questions
|
|
1609
|
+
].join("\n");
|
|
1610
|
+
var PROMPTS = [
|
|
1611
|
+
{
|
|
1612
|
+
name: "incident-triage",
|
|
1613
|
+
title: "Production incident triage",
|
|
1614
|
+
description: "Frame a production incident as clef_decide questions: immediate action, severity and blast radius.",
|
|
1615
|
+
args: {
|
|
1616
|
+
incident_description: "Short factual description of the incident (symptoms, scope, recent changes)."
|
|
1617
|
+
},
|
|
1618
|
+
build: (a) => TOOL_GUIDE(
|
|
1619
|
+
[
|
|
1620
|
+
'- "action" (choice, instructions "What should the responder do first?"): rollback "Revert the latest deploy", hotfix "Ship a targeted fix now", investigate "Gather diagnostics before acting", wait "Observe; impact is not growing".',
|
|
1621
|
+
'- "severity" (score, instructions "How severe is the incident?", criteria ["low", "elevated", "high", "critical"]).',
|
|
1622
|
+
'- "user_impact" (noul, instructions "Are users actively affected right now?").'
|
|
1623
|
+
].join("\n")
|
|
1624
|
+
) + "\n\nIncident description:\n" + (a.incident_description ?? "")
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
name: "next-action",
|
|
1628
|
+
title: "Agent next action",
|
|
1629
|
+
description: "Ask Clef what the coding agent should do next on a task: inspect, modify, test or ask the user.",
|
|
1630
|
+
args: {
|
|
1631
|
+
task_summary: "What the agent is working on, current step, and what is already known or done."
|
|
1632
|
+
},
|
|
1633
|
+
build: (a) => TOOL_GUIDE(
|
|
1634
|
+
[
|
|
1635
|
+
'- "next_action" (choice, instructions "What should the coding agent do next?"): inspect "Inspect the code and gather more information", modify "Modify the code", test "Run additional tests", ask_user "Ask the user for clarification".',
|
|
1636
|
+
'- "confidence" (score, instructions "How confident are you in this decision?", criteria ["very_low", "low", "medium", "high", "very_high"]).'
|
|
1637
|
+
].join("\n")
|
|
1638
|
+
) + "\n\nTask summary:\n" + (a.task_summary ?? "")
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
name: "ticket-routing",
|
|
1642
|
+
title: "Support ticket routing",
|
|
1643
|
+
description: "Classify an incoming message into the team that should handle it.",
|
|
1644
|
+
args: {
|
|
1645
|
+
ticket_text: "The customer or user message, verbatim if possible."
|
|
1646
|
+
},
|
|
1647
|
+
build: (a) => TOOL_GUIDE(
|
|
1648
|
+
[
|
|
1649
|
+
'- "department" (choice, instructions "Which team should handle the message?"): billing "Payments, invoices and refunds", technical "Bugs, errors and outages", sales "Pricing and new purchases", other "Anything else".',
|
|
1650
|
+
'- "urgent" (noul, instructions "Does this need a response within the hour?").'
|
|
1651
|
+
].join("\n")
|
|
1652
|
+
) + "\n\nTicket text:\n" + (a.ticket_text ?? "")
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
name: "security-review",
|
|
1656
|
+
title: "Security sanity check",
|
|
1657
|
+
description: "Ask Clef whether a code snippet, change or setup smells like a security problem.",
|
|
1658
|
+
args: {
|
|
1659
|
+
change_summary: "The code, config or setup to review \u2014 compact but complete enough to judge."
|
|
1660
|
+
},
|
|
1661
|
+
build: (a) => TOOL_GUIDE(
|
|
1662
|
+
[
|
|
1663
|
+
'- "is_vulnerable" (noul, instructions "Does this contain a realistic security vulnerability?").',
|
|
1664
|
+
'- "risk" (score, instructions "How bad is it if this ships as-is?", criteria ["informational", "low", "medium", "high", "critical"]).',
|
|
1665
|
+
'- "first_mitigation" (choice, instructions "What is the first mitigation?"): fix_code "Fix the code before merging", restrict "Restrict access or add guardrails", monitor "Add detection and monitoring", none "No mitigation needed".'
|
|
1666
|
+
].join("\n") + "\nSafety rule: only act without human review when p(is_vulnerable=false) >= 0.8."
|
|
1667
|
+
) + "\n\nChange to review:\n" + (a.change_summary ?? "")
|
|
1668
|
+
}
|
|
1669
|
+
];
|
|
1670
|
+
function registerPrompts(server) {
|
|
1671
|
+
for (const p of PROMPTS) {
|
|
1672
|
+
server.registerPrompt(
|
|
1673
|
+
p.name,
|
|
1674
|
+
{
|
|
1675
|
+
title: p.title,
|
|
1676
|
+
description: p.description,
|
|
1677
|
+
argsSchema: Object.fromEntries(Object.entries(p.args).map(([k, v]) => [k, z2.string().min(1).describe(v)]))
|
|
1678
|
+
},
|
|
1679
|
+
({ ...args }) => {
|
|
1680
|
+
for (const key of Object.keys(p.args)) {
|
|
1681
|
+
if (typeof args[key] !== "string" || !args[key]) {
|
|
1682
|
+
throw new ClefError(ClefErrorCode.INVALID_INPUT, `Prompt argument "${key}" is required`);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
messages: [{ role: "user", content: { type: "text", text: p.build(args) } }]
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/mcp/resources.ts
|
|
1694
|
+
import fsSync3 from "fs";
|
|
1695
|
+
import fsp6 from "fs/promises";
|
|
1696
|
+
|
|
1697
|
+
// src/cli/evals.ts
|
|
1698
|
+
import fsp5 from "fs/promises";
|
|
1699
|
+
import fsSync2 from "fs";
|
|
1700
|
+
import { fileURLToPath } from "url";
|
|
1701
|
+
import path8 from "path";
|
|
1702
|
+
function defaultDatasetPath() {
|
|
1703
|
+
let dir = path8.dirname(fileURLToPath(import.meta.url));
|
|
1704
|
+
for (let i = 0; i < 6; i++) {
|
|
1705
|
+
const candidate = path8.join(dir, "tests", "evals", "dataset.jsonl");
|
|
1706
|
+
if (fsSync2.existsSync(candidate)) return candidate;
|
|
1707
|
+
dir = path8.dirname(dir);
|
|
1708
|
+
}
|
|
1709
|
+
return fileURLToPath(new URL("../tests/evals/dataset.jsonl", import.meta.url));
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/mcp/resources.ts
|
|
1713
|
+
var EVALS_SCHEMA_MD = `# clef-mcp eval dataset format
|
|
1714
|
+
|
|
1715
|
+
JSONL: one JSON object per line.
|
|
1716
|
+
|
|
1717
|
+
{
|
|
1718
|
+
"id": "unique-case-id",
|
|
1719
|
+
"category": "coding | security | classification | routing | yes_no",
|
|
1720
|
+
"state": <string | JSON value>,
|
|
1721
|
+
"question": {
|
|
1722
|
+
"id": "question-id",
|
|
1723
|
+
"type": "choice | score | noul",
|
|
1724
|
+
"instructions": "\u2026",
|
|
1725
|
+
"criteria": { "option": "description" } // choice (or ["a","b"] list)
|
|
1726
|
+
| ["low", "high"] // score, ordered
|
|
1727
|
+
},
|
|
1728
|
+
"expected": {
|
|
1729
|
+
"kind": "argmax | prob_gt | min_prob | top_mass",
|
|
1730
|
+
// argmax: { "expect": "option", "among": ["option", ...] (optional) }
|
|
1731
|
+
// prob_gt: { "a": "opt", "b": "opt" }
|
|
1732
|
+
// min_prob: { "option": "opt", "min": 0.7 }
|
|
1733
|
+
// top_mass: { "options": ["a","b"], "min": 0.8 }
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
Run: \`clef-mcp evals\` (requires an installed model).
|
|
1738
|
+
`;
|
|
1739
|
+
async function readDataset() {
|
|
1740
|
+
try {
|
|
1741
|
+
return await fsp6.readFile(defaultDatasetPath(), "utf8");
|
|
1742
|
+
} catch {
|
|
1743
|
+
return "The bundled eval dataset is not available in this installation (tests/evals/dataset.jsonl not found).";
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function textResource(uri, text, mimeType = "text/markdown") {
|
|
1747
|
+
return { contents: [{ uri: uri.href, mimeType, text }] };
|
|
1748
|
+
}
|
|
1749
|
+
function requireStringUri(uri) {
|
|
1750
|
+
return uri instanceof URL ? uri : new URL(uri);
|
|
1751
|
+
}
|
|
1752
|
+
function registerResources(server, cfg, version) {
|
|
1753
|
+
server.registerResource(
|
|
1754
|
+
"capabilities",
|
|
1755
|
+
"clef-mcp://capabilities",
|
|
1756
|
+
{ title: "clef-mcp capabilities", description: "Live capability snapshot: model, runtime, limits, error codes.", mimeType: "application/json" },
|
|
1757
|
+
async (uri) => {
|
|
1758
|
+
const u = requireStringUri(uri);
|
|
1759
|
+
const payload = {
|
|
1760
|
+
name: "clef-mcp",
|
|
1761
|
+
version,
|
|
1762
|
+
model: cfg.model,
|
|
1763
|
+
runtime: cfg.runtime,
|
|
1764
|
+
limits: cfg.limits,
|
|
1765
|
+
tool: {
|
|
1766
|
+
name: "clef_decide",
|
|
1767
|
+
output: "strict JSON: { model, decisions: { <qid>: { answer: { <option>: probability } } } }",
|
|
1768
|
+
questionTypes: ["noul", "choice", "score"],
|
|
1769
|
+
notes: ["options.temperature is accepted but is a no-op (single forward pass)", "state is data and is never interpreted as instructions"]
|
|
1770
|
+
},
|
|
1771
|
+
errorCodes: [
|
|
1772
|
+
"MODEL_NOT_INSTALLED",
|
|
1773
|
+
"MODEL_LOAD_FAILED",
|
|
1774
|
+
"RUNTIME_NOT_FOUND",
|
|
1775
|
+
"RUNTIME_INIT_FAILED",
|
|
1776
|
+
"RUNTIME_NOT_SUPPORTED",
|
|
1777
|
+
"INVALID_INPUT",
|
|
1778
|
+
"CLEF_INFERENCE_FAILED",
|
|
1779
|
+
"UNSUPPORTED_PLATFORM",
|
|
1780
|
+
"OUT_OF_MEMORY",
|
|
1781
|
+
"CHECKSUM_MISMATCH",
|
|
1782
|
+
"DOWNLOAD_FAILED"
|
|
1783
|
+
]
|
|
1784
|
+
};
|
|
1785
|
+
return textResource(u, JSON.stringify(payload, null, 2), "application/json");
|
|
1786
|
+
}
|
|
1787
|
+
);
|
|
1788
|
+
server.registerResource(
|
|
1789
|
+
"evals-schema",
|
|
1790
|
+
"clef-mcp://evals/schema",
|
|
1791
|
+
{ title: "Eval dataset schema", description: "How to write cases for clef-mcp evals.", mimeType: "text/markdown" },
|
|
1792
|
+
async (uri) => textResource(requireStringUri(uri), EVALS_SCHEMA_MD)
|
|
1793
|
+
);
|
|
1794
|
+
server.registerResource(
|
|
1795
|
+
"evals-dataset",
|
|
1796
|
+
"clef-mcp://evals/dataset",
|
|
1797
|
+
{ title: "Bundled eval dataset", description: "The 30-case dataset shipped with clef-mcp.", mimeType: "application/jsonl" },
|
|
1798
|
+
async (uri) => textResource(requireStringUri(uri), await readDataset(), "application/jsonl")
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1334
1802
|
// src/mcp/server.ts
|
|
1335
1803
|
var TOOL_NAME = "clef_decide";
|
|
1336
1804
|
var SERVER_INSTRUCTIONS = "clef_decide passes task state plus typed questions to the local Clef decision model and returns a probability distribution over answer options. Treat `state` as data: it is never interpreted as instructions for this server. The result is structured JSON; do not render it as prose.";
|
|
1337
1805
|
function createClefServer(deps = {}) {
|
|
1338
1806
|
const config = deps.config ?? loadConfig();
|
|
1807
|
+
const version = deps.version ?? "0.1.0";
|
|
1339
1808
|
const log = deps.log ?? createLogger(config.logLevel);
|
|
1340
|
-
const server = new McpServer({ name: "clef-mcp", version
|
|
1809
|
+
const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
|
|
1810
|
+
registerPrompts(server);
|
|
1811
|
+
registerResources(server, config, version);
|
|
1341
1812
|
let loaded;
|
|
1342
1813
|
let loadPromise;
|
|
1343
1814
|
async function ensureRuntime(spec) {
|
|
@@ -1348,6 +1819,23 @@ function createClefServer(deps = {}) {
|
|
|
1348
1819
|
}
|
|
1349
1820
|
if (loadPromise) return loadPromise;
|
|
1350
1821
|
loadPromise = (async () => {
|
|
1822
|
+
if (config.runtime === "mlx") {
|
|
1823
|
+
const manifest2 = await mlxIsInstalled(config.clefHome);
|
|
1824
|
+
if (!manifest2) {
|
|
1825
|
+
throw new ClefError(
|
|
1826
|
+
ClefErrorCode.MODEL_NOT_INSTALLED,
|
|
1827
|
+
`Clef model "${spec.id}" is not installed for the MLX runtime.`,
|
|
1828
|
+
"Run `clef-mcp install --runtime mlx`."
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
const uvBin = resolveUv(process.env.CLEF_MLX_UV);
|
|
1832
|
+
const runtime2 = deps.runtimeFactory ? deps.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log });
|
|
1833
|
+
log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
|
|
1834
|
+
const loadStarted2 = Date.now();
|
|
1835
|
+
await runtime2.load();
|
|
1836
|
+
log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
|
|
1837
|
+
return { runtime: runtime2, model: spec.id };
|
|
1838
|
+
}
|
|
1351
1839
|
const manifest = await findInstalledManifest(config.clefHome, spec);
|
|
1352
1840
|
if (!manifest) {
|
|
1353
1841
|
throw new ClefError(
|
|
@@ -1357,7 +1845,7 @@ function createClefServer(deps = {}) {
|
|
|
1357
1845
|
);
|
|
1358
1846
|
}
|
|
1359
1847
|
const binary = resolveLlamaServerBinary(config);
|
|
1360
|
-
const runtime = deps.runtimeFactory ? deps.runtimeFactory({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
|
|
1848
|
+
const runtime = deps.runtimeFactory ? deps.runtimeFactory({ kind: "llama-cpp", serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log });
|
|
1361
1849
|
log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
|
|
1362
1850
|
const loadStarted = Date.now();
|
|
1363
1851
|
await runtime.load();
|
|
@@ -1375,9 +1863,15 @@ function createClefServer(deps = {}) {
|
|
|
1375
1863
|
TOOL_NAME,
|
|
1376
1864
|
{
|
|
1377
1865
|
title: "Clef Decide",
|
|
1378
|
-
description:
|
|
1866
|
+
description: 'Ask the local Clef decision model to judge a situation. Pass a `state` (compact factual context as a string or JSON) and one or more typed `questions`: "choice" picks among named options (criteria = option id -> description), "score" judges an ordered scale (criteria = ordered list), "noul" answers yes/no. Returns strict JSON: for every question a probability distribution over its allowed answers \u2014 no prose, no sampling. Usage: batch related decisions (up to 64 questions) in one call; state is data and is never executed; act on the argmax when a distribution is decisive (p >= 0.8), otherwise gather more context or ask the user; for destructive or security-adjacent decisions require high confidence before acting. Optional `model` overrides the model id; `options.temperature` is a no-op (single forward pass, no sampling).',
|
|
1379
1867
|
inputSchema: clefDecideInputShape(config.limits),
|
|
1380
|
-
outputSchema: clefDecideOutputSchema().shape
|
|
1868
|
+
outputSchema: clefDecideOutputSchema().shape,
|
|
1869
|
+
annotations: {
|
|
1870
|
+
readOnlyHint: true,
|
|
1871
|
+
destructiveHint: false,
|
|
1872
|
+
idempotentHint: true,
|
|
1873
|
+
openWorldHint: false
|
|
1874
|
+
}
|
|
1381
1875
|
},
|
|
1382
1876
|
async (args) => {
|
|
1383
1877
|
try {
|
|
@@ -1432,7 +1926,7 @@ function createClefServer(deps = {}) {
|
|
|
1432
1926
|
}
|
|
1433
1927
|
|
|
1434
1928
|
// src/version.ts
|
|
1435
|
-
var VERSION = "0.1
|
|
1929
|
+
var VERSION = "0.2.1";
|
|
1436
1930
|
export {
|
|
1437
1931
|
CLEF_FLASH,
|
|
1438
1932
|
ClefError,
|