clef-mcp 0.2.0 → 0.3.0

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/index.d.ts CHANGED
@@ -32,6 +32,8 @@ interface ClefConfig {
32
32
  llamaBin?: string;
33
33
  /** Pin the llama.cpp release tag used by the managed runtime download. */
34
34
  llamaReleaseTag?: string;
35
+ /** Daemon: unload the model after this many idle seconds (CLEF_DAEMON_IDLE, 0 = never). */
36
+ daemonIdleSeconds: number;
35
37
  limits: Limits;
36
38
  }
37
39
  declare function defaultClefHome(): string;
@@ -118,11 +120,22 @@ interface ClefDecideInput {
118
120
  }
119
121
  /** Probability distribution over answer options for one question. */
120
122
  type ClefDecisionAnswer = Record<string, number>;
123
+ /**
124
+ * Model-reported token usage / latency. Fields are optional because runtimes
125
+ * report different subsets (llama.cpp: tokens; the MLX loader: latency_ms).
126
+ */
127
+ type ClefUsage = {
128
+ input_tokens?: number;
129
+ output_tokens?: number;
130
+ latency_ms?: number;
131
+ };
121
132
  type ClefDecideOutput = {
122
133
  model: string;
123
134
  decisions: Record<string, {
124
135
  answer: ClefDecisionAnswer;
136
+ confidence?: number;
125
137
  }>;
138
+ usage?: ClefUsage;
126
139
  };
127
140
 
128
141
  /**
@@ -386,6 +399,16 @@ declare function detectPlatform(): Promise<PlatformInfo>;
386
399
  declare function assertSupportedPlatform(info: Pick<PlatformInfo, 'platform' | 'arch'>): void;
387
400
  declare function formatBytes(bytes: number): string;
388
401
 
402
+ /** Test seam: override runtime construction (defaults per config.runtime). */
403
+ type RuntimeFactory = (opts: {
404
+ kind: 'llama-cpp' | 'mlx';
405
+ serverBin?: string;
406
+ modelPath?: string;
407
+ snapshotPath?: string;
408
+ alias: string;
409
+ log?: Logger;
410
+ }) => ClefRuntime;
411
+
389
412
  declare const TOOL_NAME = "clef_decide";
390
413
  declare const SERVER_INSTRUCTIONS: string;
391
414
  interface ClefServerDeps {
@@ -393,14 +416,7 @@ interface ClefServerDeps {
393
416
  version?: string;
394
417
  log?: Logger;
395
418
  /** Test seam: override runtime construction (defaults per config.runtime). */
396
- runtimeFactory?: (opts: {
397
- kind: 'llama-cpp' | 'mlx';
398
- serverBin?: string;
399
- modelPath?: string;
400
- snapshotPath?: string;
401
- alias: string;
402
- log?: Logger;
403
- }) => ClefRuntime;
419
+ runtimeFactory?: RuntimeFactory;
404
420
  }
405
421
  interface RunningClefServer {
406
422
  server: McpServer;
@@ -430,11 +446,17 @@ declare function clefDecideOutputSchema(): z.ZodObject<{
430
446
  model: z.ZodString;
431
447
  decisions: z.ZodRecord<z.ZodString, z.ZodObject<{
432
448
  answer: z.ZodRecord<z.ZodString, z.ZodNumber>;
449
+ confidence: z.ZodOptional<z.ZodNumber>;
450
+ }, z.core.$strip>>;
451
+ usage: z.ZodOptional<z.ZodObject<{
452
+ input_tokens: z.ZodOptional<z.ZodNumber>;
453
+ output_tokens: z.ZodOptional<z.ZodNumber>;
454
+ latency_ms: z.ZodOptional<z.ZodNumber>;
433
455
  }, z.core.$strip>>;
434
456
  }, z.core.$strip>;
435
457
  /** `state` cannot be size-checked inside zod (it is `unknown`), so it is validated separately. */
436
458
  declare function validateStateSize(state: unknown, limits: Limits): void;
437
459
 
438
- declare const VERSION = "0.2.0";
460
+ declare const VERSION = "0.3.0";
439
461
 
440
- export { CLEF_FLASH, type ClefConfig, type ClefDecideInput, type ClefDecideOutput, type ClefDecisionAnswer, ClefError, ClefErrorCode, type ClefQuestion, type ClefRuntime, type ClefServerDeps, DEFAULT_LIMITS, type DownloadProgress, type InstallModelResult, type Limits, LlamaCppRuntime, type LlamaCppRuntimeOptions, type LogLevel, type Logger, MODELS, type ModelManifest, type ModelSpec, type PlatformInfo, type ProbeResult, type QuantSpec, type QuestionType, type ResolvedBinary, type RunningClefServer, type RuntimeHealth, SERVER_INSTRUCTIONS, StderrLogger, type SystemOneRequest, type SystemOneResponse, TOOL_NAME, VERSION, assertSupportedPlatform, clefDecideInputSchema, clefDecideOutputSchema, createClefServer, createLogger, deepVerifyChecksum, defaultClefHome, detectPlatform, downloadFile, downloadManagedLlama, ensureLlamaRuntime, errorJson, exitCodeFor, fetchFileInfo, findInstalledManifest, formatBytes, fromSystemOneResponse, getModelSpec, getQuantSpec, installModel, limitsFromEnv, loadConfig, manifestPathFor, modelQuantDir, pathsFor, pickQuant, probeInference, readModelManifest, removeManagedRuntime, resolveLlamaServerBinary, toSystemOneRequest, validateStateSize, verifyModelFiles, writeModelManifest };
462
+ export { CLEF_FLASH, type ClefConfig, type ClefDecideInput, type ClefDecideOutput, type ClefDecisionAnswer, ClefError, ClefErrorCode, type ClefQuestion, type ClefRuntime, type ClefServerDeps, type ClefUsage, DEFAULT_LIMITS, type DownloadProgress, type InstallModelResult, type Limits, LlamaCppRuntime, type LlamaCppRuntimeOptions, type LogLevel, type Logger, MODELS, type ModelManifest, type ModelSpec, type PlatformInfo, type ProbeResult, type QuantSpec, type QuestionType, type ResolvedBinary, type RunningClefServer, type RuntimeHealth, SERVER_INSTRUCTIONS, StderrLogger, type SystemOneRequest, type SystemOneResponse, TOOL_NAME, VERSION, assertSupportedPlatform, clefDecideInputSchema, clefDecideOutputSchema, createClefServer, createLogger, deepVerifyChecksum, defaultClefHome, detectPlatform, downloadFile, downloadManagedLlama, ensureLlamaRuntime, errorJson, exitCodeFor, fetchFileInfo, findInstalledManifest, formatBytes, fromSystemOneResponse, getModelSpec, getQuantSpec, installModel, limitsFromEnv, loadConfig, manifestPathFor, modelQuantDir, pathsFor, pickQuant, probeInference, readModelManifest, removeManagedRuntime, resolveLlamaServerBinary, toSystemOneRequest, validateStateSize, verifyModelFiles, writeModelManifest };
package/dist/index.js CHANGED
@@ -48,6 +48,7 @@ function loadConfig(env = process.env) {
48
48
  const runtime = runtimeRaw;
49
49
  const llamaBin = env.CLEF_LLAMA_BIN?.trim() || void 0;
50
50
  const llamaReleaseTag = env.CLEF_LLAMA_RELEASE_TAG?.trim() || void 0;
51
+ const idleRaw = Number.parseInt(env.CLEF_DAEMON_IDLE?.trim() ?? "", 10);
51
52
  return {
52
53
  model: env.CLEF_MODEL?.trim() || "clef-flash",
53
54
  clefHome: env.CLEF_HOME?.trim() || defaultClefHome(),
@@ -55,6 +56,7 @@ function loadConfig(env = process.env) {
55
56
  logLevel,
56
57
  llamaBin: runtime === "llama-cpp" ? llamaBin : void 0,
57
58
  llamaReleaseTag: runtime === "llama-cpp" ? llamaReleaseTag : void 0,
59
+ daemonIdleSeconds: Number.isFinite(idleRaw) && idleRaw >= 0 ? idleRaw : 600,
58
60
  limits: limitsFromEnv(env)
59
61
  };
60
62
  }
@@ -333,6 +335,21 @@ function normalizeScore(ans, qid, question) {
333
335
  function normalizeChoice(ans, qid) {
334
336
  return probabilitiesMap(ans, qid);
335
337
  }
338
+ function confidenceOf(ans) {
339
+ return typeof ans.confidence === "number" && Number.isFinite(ans.confidence) ? ans.confidence : void 0;
340
+ }
341
+ function normalizeUsage(usage) {
342
+ if (!usage || typeof usage !== "object") return void 0;
343
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
344
+ const out = {};
345
+ const input = num(usage.input_tokens) ?? num(usage.prompt_tokens);
346
+ const output = num(usage.output_tokens) ?? num(usage.completion_tokens);
347
+ const latency = num(usage.latency_ms);
348
+ if (input !== void 0) out.input_tokens = input;
349
+ if (output !== void 0) out.output_tokens = output;
350
+ if (latency !== void 0) out.latency_ms = latency;
351
+ return Object.keys(out).length > 0 ? out : void 0;
352
+ }
336
353
  function fromSystemOneResponse(response, questions, fallbackModel) {
337
354
  const answers = response.answers;
338
355
  if (!answers || typeof answers !== "object") {
@@ -353,9 +370,11 @@ function fromSystemOneResponse(response, questions, fallbackModel) {
353
370
  answer = normalizeScore(ans, qid, question);
354
371
  break;
355
372
  }
356
- decisions[qid] = { answer };
373
+ const confidence = confidenceOf(ans);
374
+ decisions[qid] = confidence === void 0 ? { answer } : { answer, confidence };
357
375
  }
358
- return { model: response.model ?? fallbackModel, decisions };
376
+ const usage = normalizeUsage(response.usage);
377
+ return usage === void 0 ? { model: response.model ?? fallbackModel, decisions } : { model: response.model ?? fallbackModel, decisions, usage };
359
378
  }
360
379
 
361
380
  // src/runtime/llama-cpp.ts
@@ -1470,6 +1489,75 @@ function formatBytes(bytes) {
1470
1489
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1471
1490
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1472
1491
 
1492
+ // src/runtime/loader.ts
1493
+ var RuntimeLoader = class {
1494
+ constructor(config, log, runtimeFactory) {
1495
+ this.config = config;
1496
+ this.log = log;
1497
+ this.runtimeFactory = runtimeFactory;
1498
+ }
1499
+ config;
1500
+ log;
1501
+ runtimeFactory;
1502
+ loaded;
1503
+ loadPromise;
1504
+ async ensure(spec) {
1505
+ if (this.loaded && this.loaded.model === spec.id) return this.loaded;
1506
+ if (this.loaded && this.loaded.model !== spec.id) {
1507
+ await this.loaded.runtime.unload();
1508
+ this.loaded = void 0;
1509
+ }
1510
+ if (this.loadPromise) return this.loadPromise;
1511
+ this.loadPromise = (async () => {
1512
+ if (this.config.runtime === "mlx") {
1513
+ const manifest2 = await mlxIsInstalled(this.config.clefHome);
1514
+ if (!manifest2) {
1515
+ throw new ClefError(
1516
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1517
+ `Clef model "${spec.id}" is not installed for the MLX runtime.`,
1518
+ "Run `clef-mcp install --runtime mlx`."
1519
+ );
1520
+ }
1521
+ const uvBin = resolveUv(process.env.CLEF_MLX_UV);
1522
+ const runtime2 = this.runtimeFactory ? this.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log: this.log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log: this.log });
1523
+ this.log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
1524
+ const loadStarted2 = Date.now();
1525
+ await runtime2.load();
1526
+ this.log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
1527
+ return { runtime: runtime2, model: spec.id };
1528
+ }
1529
+ const manifest = await findInstalledManifest(this.config.clefHome, spec);
1530
+ if (!manifest) {
1531
+ throw new ClefError(
1532
+ ClefErrorCode.MODEL_NOT_INSTALLED,
1533
+ `Clef model "${spec.id}" is not installed.`,
1534
+ "Run `clef-mcp install`."
1535
+ );
1536
+ }
1537
+ const binary = resolveLlamaServerBinary(this.config);
1538
+ const runtime = this.runtimeFactory ? this.runtimeFactory({ kind: "llama-cpp", serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log: this.log }) : new LlamaCppRuntime({ serverBin: binary.path, modelPath: manifest.modelPath, alias: spec.id, log: this.log });
1539
+ this.log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
1540
+ const loadStarted = Date.now();
1541
+ await runtime.load();
1542
+ this.log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
1543
+ return { runtime, model: spec.id };
1544
+ })();
1545
+ try {
1546
+ this.loaded = await this.loadPromise;
1547
+ return this.loaded;
1548
+ } finally {
1549
+ this.loadPromise = void 0;
1550
+ }
1551
+ }
1552
+ /** Unload the current runtime (kills the llama-server subprocess, if any). */
1553
+ async dispose() {
1554
+ if (this.loaded) {
1555
+ await this.loaded.runtime.unload();
1556
+ this.loaded = void 0;
1557
+ }
1558
+ }
1559
+ };
1560
+
1473
1561
  // src/mcp/schema.ts
1474
1562
  import { z } from "zod";
1475
1563
  function criteriaDescription(limits) {
@@ -1503,10 +1591,12 @@ function scoreCriteria(limits) {
1503
1591
  }
1504
1592
  function questionSchema(limits) {
1505
1593
  return z.object({
1506
- type: z.enum(["noul", "choice", "score"]),
1507
- instructions: z.string().min(1).max(limits.maxInstructionsChars),
1594
+ type: z.enum(["noul", "choice", "score"]).describe('"choice" = pick among named options; "score" = judge an ordered scale; "noul" = yes/no question'),
1595
+ instructions: z.string().min(1).max(limits.maxInstructionsChars).describe("What to judge, phrased as a self-contained question."),
1508
1596
  criteria: z.union([z.array(z.string()), z.record(z.string(), z.string())]).optional()
1509
- }).superRefine((q, ctx) => {
1597
+ }).describe(
1598
+ "A single decision question. Allowed answers come from `criteria` and must be mutually exclusive and collectively exhaustive."
1599
+ ).superRefine((q, ctx) => {
1510
1600
  if (q.type === "choice") {
1511
1601
  const parsed = choiceCriteria(limits).safeParse(q.criteria);
1512
1602
  if (!parsed.success) {
@@ -1530,23 +1620,27 @@ function questionSchema(limits) {
1530
1620
  }
1531
1621
  function clefDecideInputShape(limits) {
1532
1622
  return {
1533
- state: z.unknown(),
1534
- questions: z.record(z.string().min(1).max(limits.maxQuestionIdChars), questionSchema(limits)).check((ctx) => {
1623
+ state: z.unknown().describe(
1624
+ "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."
1625
+ ),
1626
+ questions: z.record(z.string().min(1).max(limits.maxQuestionIdChars), questionSchema(limits)).describe(
1627
+ "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."
1628
+ ).check((ctx) => {
1535
1629
  const count = Object.keys(ctx.value).length;
1536
1630
  if (count === 0) ctx.issues.push({ code: "custom", message: "questions must not be empty", input: ctx.value });
1537
1631
  if (count > limits.maxQuestions) {
1538
1632
  ctx.issues.push({ code: "custom", message: `too many questions (max ${limits.maxQuestions})`, input: ctx.value });
1539
1633
  }
1540
1634
  }),
1541
- model: z.string().min(1).optional(),
1635
+ model: z.string().min(1).optional().describe("Model id to use (default: clef-flash from CLEF_MODEL)."),
1542
1636
  options: z.object({
1543
1637
  /**
1544
1638
  * Accepted for forward compatibility. Clef scores all options in a
1545
1639
  * single forward pass — there is no sampling, so temperature is a
1546
1640
  * documented no-op.
1547
1641
  */
1548
- temperature: z.number().min(0).max(2).optional()
1549
- }).optional()
1642
+ temperature: z.number().min(0).max(2).optional().describe("Accepted for forward compatibility; no-op. Clef scores in a single forward pass without sampling.")
1643
+ }).optional().describe("Extra options. Currently only temperature, which is a documented no-op.")
1550
1644
  };
1551
1645
  }
1552
1646
  function clefDecideInputSchema(limits) {
@@ -1555,7 +1649,20 @@ function clefDecideInputSchema(limits) {
1555
1649
  function clefDecideOutputSchema() {
1556
1650
  return z.object({
1557
1651
  model: z.string(),
1558
- decisions: z.record(z.string(), z.object({ answer: z.record(z.string(), z.number()) }))
1652
+ decisions: z.record(
1653
+ z.string(),
1654
+ z.object({
1655
+ answer: z.record(z.string(), z.number()),
1656
+ // Model-reported confidence; present only when the runtime sends it.
1657
+ confidence: z.number().min(0).max(1).optional()
1658
+ })
1659
+ ),
1660
+ // Token usage / latency; present only when the runtime reports it.
1661
+ usage: z.object({
1662
+ input_tokens: z.number().optional(),
1663
+ output_tokens: z.number().optional(),
1664
+ latency_ms: z.number().optional()
1665
+ }).optional()
1559
1666
  });
1560
1667
  }
1561
1668
  function validateStateSize(state, limits) {
@@ -1803,63 +1910,20 @@ function createClefServer(deps = {}) {
1803
1910
  const server = new McpServer({ name: "clef-mcp", version }, { instructions: SERVER_INSTRUCTIONS });
1804
1911
  registerPrompts(server);
1805
1912
  registerResources(server, config, version);
1806
- let loaded;
1807
- let loadPromise;
1808
- async function ensureRuntime(spec) {
1809
- if (loaded && loaded.model === spec.id) return loaded;
1810
- if (loaded && loaded.model !== spec.id) {
1811
- await loaded.runtime.unload();
1812
- loaded = void 0;
1813
- }
1814
- if (loadPromise) return loadPromise;
1815
- loadPromise = (async () => {
1816
- if (config.runtime === "mlx") {
1817
- const manifest2 = await mlxIsInstalled(config.clefHome);
1818
- if (!manifest2) {
1819
- throw new ClefError(
1820
- ClefErrorCode.MODEL_NOT_INSTALLED,
1821
- `Clef model "${spec.id}" is not installed for the MLX runtime.`,
1822
- "Run `clef-mcp install --runtime mlx`."
1823
- );
1824
- }
1825
- const uvBin = resolveUv(process.env.CLEF_MLX_UV);
1826
- const runtime2 = deps.runtimeFactory ? deps.runtimeFactory({ kind: "mlx", snapshotPath: manifest2.snapshotPath, alias: spec.id, log }) : new MlxRuntime({ uvBin, snapshotPath: manifest2.snapshotPath, log });
1827
- log.debug("loading model (mlx)", { model: spec.id, snapshot: manifest2.snapshotPath });
1828
- const loadStarted2 = Date.now();
1829
- await runtime2.load();
1830
- log.info("model ready", { model: spec.id, runtime: "mlx", loadMs: Date.now() - loadStarted2 });
1831
- return { runtime: runtime2, model: spec.id };
1832
- }
1833
- const manifest = await findInstalledManifest(config.clefHome, spec);
1834
- if (!manifest) {
1835
- throw new ClefError(
1836
- ClefErrorCode.MODEL_NOT_INSTALLED,
1837
- `Clef model "${spec.id}" is not installed.`,
1838
- "Run `clef-mcp install`."
1839
- );
1840
- }
1841
- const binary = resolveLlamaServerBinary(config);
1842
- 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 });
1843
- log.debug("loading model", { model: spec.id, quant: manifest.quant, bin: binary.path });
1844
- const loadStarted = Date.now();
1845
- await runtime.load();
1846
- log.info("model ready", { model: spec.id, quant: manifest.quant, loadMs: Date.now() - loadStarted });
1847
- return { runtime, model: spec.id };
1848
- })();
1849
- try {
1850
- loaded = await loadPromise;
1851
- return loaded;
1852
- } finally {
1853
- loadPromise = void 0;
1854
- }
1855
- }
1913
+ const loader = new RuntimeLoader(config, log, deps.runtimeFactory);
1856
1914
  server.registerTool(
1857
1915
  TOOL_NAME,
1858
1916
  {
1859
1917
  title: "Clef Decide",
1860
- description: "Pass task state and a set of structured questions (noul / choice / score) to the local Clef decision model. Returns a probability distribution over the allowed answers for every question. The result is structured JSON.",
1918
+ description: 'Judge a situation with the local Clef decision model.\nPass `state` (compact factual context \u2014 string or JSON; treated as data, never executed) and typed `questions`:\n- "choice": pick among named options (criteria = option id -> description)\n- "score": judge an ordered scale (criteria = ordered list of levels)\n- "noul": yes/no question\nReturns strict JSON \u2014 a probability distribution over the allowed answers for every question. No prose, no sampling.\nEach decision also carries `confidence` (model-reported, when available) and the response carries `usage`\n(input_tokens/output_tokens/latency_ms). Act on the argmax only when the distribution is decisive (top p >= 0.8\nand high confidence for destructive or security-adjacent calls); otherwise gather more context or ask the user.\nUsage: batch related decisions in one call (up to 64 questions, one forward pass).\nOptional `model` overrides the model id. `options.temperature` is a no-op: scoring is a single forward pass, nothing is sampled.',
1861
1919
  inputSchema: clefDecideInputShape(config.limits),
1862
- outputSchema: clefDecideOutputSchema().shape
1920
+ outputSchema: clefDecideOutputSchema().shape,
1921
+ annotations: {
1922
+ readOnlyHint: true,
1923
+ destructiveHint: false,
1924
+ idempotentHint: true,
1925
+ openWorldHint: false
1926
+ }
1863
1927
  },
1864
1928
  async (args) => {
1865
1929
  try {
@@ -1873,7 +1937,7 @@ function createClefServer(deps = {}) {
1873
1937
  options: parsed.options
1874
1938
  };
1875
1939
  const request = toSystemOneRequest(input, spec.id);
1876
- const { runtime } = await ensureRuntime(spec);
1940
+ const { runtime } = await loader.ensure(spec);
1877
1941
  const response = await runtime.decide(request);
1878
1942
  const output = fromSystemOneResponse(response, parsed.questions, spec.id);
1879
1943
  return {
@@ -1891,10 +1955,7 @@ function createClefServer(deps = {}) {
1891
1955
  }
1892
1956
  );
1893
1957
  async function shutdown() {
1894
- if (loaded) {
1895
- await loaded.runtime.unload();
1896
- loaded = void 0;
1897
- }
1958
+ await loader.dispose();
1898
1959
  }
1899
1960
  async function start() {
1900
1961
  const transport = new StdioServerTransport();
@@ -1914,7 +1975,7 @@ function createClefServer(deps = {}) {
1914
1975
  }
1915
1976
 
1916
1977
  // src/version.ts
1917
- var VERSION = "0.2.0";
1978
+ var VERSION = "0.3.0";
1918
1979
  export {
1919
1980
  CLEF_FLASH,
1920
1981
  ClefError,