@octocodeai/freellama-mcp-server 0.6.0 → 0.6.2

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/CONFIGURATION.md CHANGED
@@ -9,7 +9,7 @@ server-launch config (for example `.mcp.json`'s `env` block) or the launching sh
9
9
 
10
10
  | Variable | Default | Affects |
11
11
  |---|---|---|
12
- | `FREELLAMA_OLLAMA_ENDPOINT` | `http://127.0.0.1:11434` | Default Ollama endpoint for `doctor`, the `models` detail/raw/library views, and all `ollama_*` tools |
12
+ | `FREELLAMA_OLLAMA_ENDPOINT` | `http://127.0.0.1:11434` | Default Ollama endpoint for `doctor`, the `models` detail/raw/library views, all `ollama_*` tools, and the autostarted serve's `--upstream` |
13
13
  | `FREELLAMA_SERVE_ENDPOINT` | `http://127.0.0.1:11435` | Default `endpoint` for serve-backed tools and `delegate_research` |
14
14
  | `FREELLAMA_MCP_DEFAULT_MODEL` | unset (routes to an installed coding model) | `delegate_research`'s default `model` |
15
15
  | `FREELLAMA_MCP_DEFAULT_ADAPTER` | `bash` | `delegate_research`'s adapter (`octocode` to switch) |
@@ -22,11 +22,11 @@ server-launch config (for example `.mcp.json`'s `env` block) or the launching sh
22
22
  | `FREELLAMA_EXTERNAL_COST_MODEL` | unset | External-model label in estimated avoided-cost telemetry; set all three cost variables or none |
23
23
  | `FREELLAMA_EXTERNAL_COST_INPUT_USD_PER_M` | unset | External input USD per million tokens |
24
24
  | `FREELLAMA_EXTERNAL_COST_OUTPUT_USD_PER_M` | unset | External output USD per million tokens |
25
- | `FREELLAMA_CONTROL_TIMEOUT_SECONDS` | `30` | Decision-only serve calls; read by `packages/rust-core/src/napi.rs` |
26
- | `FREELLAMA_TASK_TIMEOUT_SECONDS` | `900` | Generation calls; read by `packages/rust-core/src/napi.rs` |
25
+ | `FREELLAMA_CONTROL_TIMEOUT_SECONDS` | `30` | Decision-only serve calls; read by the native client (`packages/rust-core/src/serve_client.rs`) |
26
+ | `FREELLAMA_TASK_TIMEOUT_SECONDS` | `900` | Generation calls; read by the native client (`packages/rust-core/src/serve_client.rs`) |
27
27
  | `FREELLAMA_AUTH_TOKEN_FILE` | unset | Bearer-token file used by the native client and research adapters for all serve routes |
28
28
  | `FREELLAMA_AGENT_TOKEN_CALIBRATION_DIR` | platform data directory | Prompt-free, model-specific token-estimator calibration shared across adapter processes |
29
- | `FREELLAMA_MCP_AUTOSTART_SERVE` | `1` | When the default loopback serve endpoint does not answer, start `freellama serve` as a child of the MCP server (stopped with it). `0` disables |
29
+ | `FREELLAMA_MCP_AUTOSTART_SERVE` | `1` | When the default loopback serve endpoint does not answer, start `freellama serve` as a child of the MCP server (stopped with it): once in the background at launch, so other local clients find it, and again on a tool call if it is still down. `0` disables |
30
30
  | `FREELLAMA_SERVE_BINARY` | checkout `target/release`/`debug`, then the platform package | Binary used for serve autostart |
31
31
  | `FREELLAMA_AGENT_OS_SANDBOX` | `auto` | Bash adapter OS confinement: `bwrap` on Linux, `sandbox-exec` on macOS, when present. `off` keeps only the restricted shell |
32
32
  | `FREELLAMA_AGENT_OCTOCODE_PACKAGE` | `octocode@19.1.0` | Pinned npm spec the Octocode adapter runs through `npx` |
@@ -103,6 +103,4 @@ slots or relax memory guards. Existing queued work keeps its captured cost. See
103
103
  `packages/mcp/evidence/model-evidence.json`. A published package does not include that repository
104
104
  file, so its evidence table is empty unless you set `FREELLAMA_MCP_MODEL_EVIDENCE`. An unmeasured
105
105
  model yields a `verify` verdict, and an unreadable evidence file is reported on stderr and treated
106
- as empty. Tool descriptions state the rules; measurement details live in
107
- [`skills/freellama/references/`](https://github.com/bgauryy/FreeLlama/tree/main/skills/freellama/references/) and
108
- [`docs/dev/evidence/`](https://github.com/bgauryy/FreeLlama/tree/main/docs/dev/evidence/).
106
+ as empty. Tool descriptions state the rules.
package/DEVELOPING.md CHANGED
@@ -13,10 +13,11 @@ yarn install
13
13
  yarn build
14
14
  ```
15
15
 
16
- `yarn build` writes **one** MCP file, `packages/mcp/dist/index.js` (shebang, executable). During
16
+ `yarn build` writes the MCP server, `packages/mcp/dist/index.js` (shebang, executable). During
17
17
  checkout development it loads `native/freellama.<triple>.node`. Published installs resolve the
18
18
  matching `@octocodeai/freellama-native-<target>` optional package instead, so the base MCP package remains
19
- portable. Research adapters are copied into `adapters/` for packed installs.
19
+ portable. The research adapters compile to `dist/research/{bash-agent,octocode-agent}.js`, and the
20
+ repository `docs/` guides are copied into `docs/`.
20
21
 
21
22
  Piecewise, if you are iterating on one layer:
22
23
 
@@ -24,7 +25,7 @@ Piecewise, if you are iterating on one layer:
24
25
  # 1. Native addon → packages/mcp/native/freellama.<triple>.node
25
26
  yarn build:native
26
27
 
27
- # 2. This server → dist/index.js via esbuild (then copies the research adapters into adapters/)
28
+ # 2. This server → dist/index.js and dist/research/*.js via esbuild, plus docs/
28
29
  yarn workspace @octocodeai/freellama-mcp-server build
29
30
  ```
30
31
 
@@ -33,8 +34,8 @@ root produces both in a checkout. `dist/index.js` loads `native/index.js`, which
33
34
  first and then a matching optional platform package. The `.node` binary is gitignored (a build
34
35
  artifact). `native/index.js` is a small hand-written loader; `native/index.d.ts` is generated from
35
36
  `napi.rs` by `yarn build:native`. Both are checked in; do not edit the declarations by hand —
36
- change the Rust doc comments or `ts_args_type`, rebuild, and commit. CI rebuilds it and fails
37
- on any diff.
37
+ change the Rust doc comments or `ts_args_type`, rebuild, and commit. The unit test
38
+ `test/unit/native-dts-drift.test.ts` fails when the declarations and the `napi.rs` exports differ.
38
39
 
39
40
  ## Test
40
41
 
@@ -42,15 +43,17 @@ Vitest, in three tiers (all TypeScript, under `test/`):
42
43
 
43
44
  | Tier | Command (from this package) | Checks | Needs |
44
45
  |---|---|---|---|
45
- | `test/unit/` | `yarn test` (watch: `yarn test:watch`) | pure functions straight from `src/*.ts` — no build step, this is the TDD loop | nothing, ~0.5s |
46
+ | `test/unit/` | `yarn test` (watch: `yarn test:watch`) | pure functions straight from `src/*.ts` — no build step, this is the TDD loop | nothing, a few seconds |
46
47
  | `test/integration/` | `yarn test:integration` | the built server over the real MCP protocol: tool contract, structured content, guardrails; rebuilds `dist/` itself | Ollama at `FREELLAMA_OLLAMA_ENDPOINT` (default :11434) |
47
48
  | `test/e2e/` | `yarn test:e2e` | every tool against the live system: real routing/execution, pull → delete round trip (net-zero), a real delegated research run | serve + Ollama + models, ~40s |
48
49
 
49
- The same commands work from the repository root (`yarn test` there also runs the CLI package's tests).
50
+ The same commands work from the repository root (`yarn test` there also runs the CLI package's tests
51
+ and the Rust suite).
50
52
  The integration/e2e tiers fail fast with a readable message when Ollama is down, and the lifecycle
51
53
  test refuses to delete a model already installed on the test system.
52
54
 
53
- For default-endpoint autostart, a spawn failure, early exit, or readiness timeout rejects the
55
+ For default-endpoint autostart, the server starts `freellama serve` in the background at launch and
56
+ logs a failure to stderr. On a tool call, a spawn failure, early exit, or readiness timeout rejects the
54
57
  request while leaving the MCP transport available for diagnosis or retry. Autostart does not apply
55
58
  to an explicit non-default endpoint; see `FREELLAMA_MCP_AUTOSTART_SERVE` in
56
59
  [configuration](CONFIGURATION.md#environment-variables).
@@ -74,8 +77,8 @@ Start the serve-backed tools' dependency from the repository root:
74
77
  cargo run --release -- serve
75
78
  ```
76
79
 
77
- See `skills/freellama/references/proxy-vs-serve.md` for the `proxy` vs `serve` distinction (only
78
- `serve` has the `/_freellama/v1/*` routes these tools call).
80
+ `freellama proxy` only forwards raw `/api/*` calls; only `serve` has the `/_freellama/v1/*` routes
81
+ these tools call.
79
82
 
80
83
  ## Publish
81
84
 
@@ -91,18 +94,18 @@ cd packages/mcp
91
94
  npm pack --dry-run # confirm exactly what a publish would ship
92
95
  ```
93
96
 
94
- Treat `npm publish` as a public release action. Follow the repository
95
- [release procedure](https://github.com/bgauryy/FreeLlama/blob/main/RELEASE.md) and obtain the repository owner's release authorization first.
97
+ Treat `npm publish` as a public release action. Follow the
98
+ [release package checks](https://github.com/bgauryy/FreeLlama/blob/main/docs/TESTING.md#verify-release-packages) and obtain the repository owner's release authorization first.
96
99
 
97
100
  ## Why native bindings
98
101
 
99
102
  - **No routing reimplementation.** `run_task`, `run_task {preview:true}`, and the installed-model list are HTTP
100
- calls into a running `freellama serve`, matching the CLI tool map in `packages/cli/src/tools.rs`. `doctor` talks to Ollama
103
+ calls into a running `freellama serve`, matching the CLI requests in `packages/cli/src/requests.rs`. `doctor` talks to Ollama
101
104
  and the host's portable machine-discovery layer directly. `ollama_manage` / `ollama_delete` hit Ollama's HTTP API. `delegate_research`
102
105
  is a subprocess. Routing decisions still live in exactly one place — the Rust core; see
103
106
  [what the MCP layer computes](README.md#what-the-mcp-layer-computes).
104
- - **One connection pool.** `packages/rust-core/src/napi.rs` holds a single `reqwest::Client` in a
105
- `OnceLock` and clones the handle per request, matching the server side.
107
+ - **One connection pool.** `packages/rust-core/src/serve_client.rs` holds a single `reqwest::Client` in a
108
+ `OnceLock` and clones the handle per request; `napi.rs` and the CLI share it.
106
109
  - **`unsafe_code` stays isolated.** The crate is `deny(unsafe_code)` except `napi.rs`, which
107
110
  carries an explicit `#[allow(unsafe_code)]` for napi-derive's FFI glue.
108
111
  - **The napi build is off by default** (`cargo build` never touches it); the addon is built with
package/LICENSE-MIT CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) FreeLlama contributors
3
+ Copyright (c) 2026 Guy Bary
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
6
  associated documentation files (the "Software"), to deal in the Software without restriction, including
package/README.md CHANGED
@@ -14,7 +14,7 @@ The server uses the official [TypeScript SDK](https://github.com/modelcontextpro
14
14
 
15
15
  ## Understand the tools
16
16
 
17
- `doctor`, `models`, `run_task`, and `task_jobs` call the Rust core through native NAPI bindings
17
+ `doctor`, `models`, `run_task`, `task_jobs`, `scope`, and `session` call the Rust core through native NAPI bindings
18
18
  (`packages/rust-core/src/napi.rs`), with no CLI subprocess and no routing decisions in TypeScript; the
19
19
  TypeScript layer validates request shapes and derives presentation fields (see
20
20
  [what the MCP layer computes](#what-the-mcp-layer-computes)). `run_task` has four modes: one task,
@@ -190,7 +190,7 @@ flowchart TD
190
190
  S --> G["Primary Ollama"]
191
191
  S --> U["Optional CPU Ollama"]
192
192
  B -->|"detail, raw; manage; delete"| G
193
- B -->|"library view"| W["ollama.com library"]
193
+ B -->|"library view"| W["ollama.com library + Hugging Face GGUF"]
194
194
  B -->|"delegate_research"| A["allowlisted local adapter subprocess"]
195
195
  A --> S
196
196
  C -->|"resources/read on demand"| D["packaged docs/*.md"]
@@ -218,9 +218,8 @@ The TypeScript server adds a small set of derived fields on top of core results:
218
218
 
219
219
  ## Read packaged documentation through MCP
220
220
 
221
- The published MCP package includes the top-level Markdown guides from the repository `docs/`
222
- directory (developer notes under `docs/dev/` are not packaged). The repository directory remains the
223
- source of truth; the package build copies it into `docs/`.
221
+ The published MCP package includes the Markdown guides from the repository `docs/` directory.
222
+ The repository directory remains the source of truth; the package build copies it into `docs/`.
224
223
 
225
224
  MCP clients can list resources and read `freellama://docs/index` (the repository `docs/README.md`),
226
225
  then fetch exactly one relevant guide such as `freellama://docs/PRODUCTION` or
@@ -243,6 +242,8 @@ node packages/mcp/dist/index.js
243
242
  ```
244
243
 
245
244
  The server speaks MCP over stdio — configure it in your MCP client, do not run it interactively.
245
+ At launch it starts `freellama serve` in the background when the default endpoint does not answer;
246
+ see `FREELLAMA_MCP_AUTOSTART_SERVE` in [Configuration](CONFIGURATION.md#environment-variables).
246
247
 
247
248
  **Claude Desktop (`claude_desktop_config.json`):**
248
249
 
@@ -264,7 +265,7 @@ The server speaks MCP over stdio — configure it in your MCP client, do not run
264
265
  | Tool | Needs `freellama serve`? | What it does |
265
266
  |---|---|---|
266
267
  | `doctor` | No (except `status`/`usage`) | Runtime/configuration diagnostic. Target another Ollama with `ollamaEndpoint` and serve with `serveEndpoint`. `summary` (default) returns endpoint, version, resident count, host/profile signals, and next step; `full` adds all non-duplicated diagnostics, including admission and categorized Ollama settings; `status` returns serve's live view (per-backend queues, current and adaptive limits, circuit breakers, loaded models, host memory, Ollama's effective settings, today's usage); `usage` returns task and token totals per day and model (`days`, default 7). The report distinguishes an observed same-user macOS process from configuration hints and never claims remote process visibility. |
267
- | `models` | `installed` (default) and `resident` do; `detail`/`raw`/`library` don't | Views: `installed` (capabilities, use explanations, derived `model_type`, VRAM, context, policy_rank), `resident` (loaded now + managed GPU/CPU split), `detail` (one model, maximum context), paged `raw` (`GET /api/tags`), `library` (two-step ollama.com search: omit `model` for families, pass `model:"<family>"` for all tags/`fitsInMemory`). `includeLibrary:true` enriches installed/detail views with sourced public guidance; `includeReadme:true` returns bounded README text in library step 2 or enriched detail. |
268
+ | `models` | `installed` (default) and `resident` do; `detail`/`raw`/`library` don't | Views: `installed` (capabilities, use explanations, derived `model_type`, VRAM, context, policy_rank), `resident` (loaded now + managed GPU/CPU split), `detail` (one model, maximum context), paged `raw` (`GET /api/tags`), `library` (two-step public search: omit `model` to search ollama.com families and, for a `query`, Hugging Face GGUF repos (`source` limits it to one; `limit` applies per source); pass `model:"<family>"` or `model:"hf.co/<owner>/<repo>"` for every pullable tag with its size and `fitsInMemory`). `includeLibrary:true` enriches installed/detail views with sourced public guidance; `includeReadme:true` returns bounded README text in library step 2 or enriched detail. |
268
269
  | `run_task` | Yes | **Routes or executes** a chat/generate/embed call. `preview: true` accepts routing fields only and returns a decision without generation. Execution omits preview and supplies the payload. Embedding results withhold raw vectors by default (`returnEmbeddings: true` to get them). `tasks: [{id, independent:true, task}]` (max 64, `maxParallelism` bounds fan-out) runs a batch whose nested `task` takes the same fields; it is not a dependency scheduler. `warm: true` warms an exact installed `model` through managed admission with residency/profile controls and optional `defer:true`. |
269
270
  | `scope` | Yes | Creates, inspects, forks, or deletes process-local message history; history inclusion requires `includeMessages:true`. |
270
271
  | `session` | Yes | Creates, deletes, or kills model affinity without storing messages or KV; `kill` cancels the session's in-flight work. |
@@ -318,8 +319,8 @@ Memory admission, authentication, and capability requirements apply in every mod
318
319
  tasks or `input` for embeddings, plus any applicable Ollama runtime controls.
319
320
 
320
321
  Preview rejects `prompt`, `systemPrompt`, `messages`, `input`, `images`, `tools`, `keepAlive`, `format`, `think`,
321
- `options`, `logprobs`, `topLogprobs`, `returnEmbeddings`, `priority`, `maxWaitSeconds`, `timeoutSeconds`, and
322
- `defer`, `scopeId`, and `scopeRevision`. This prevents a client from attaching
322
+ `options`, `logprobs`, `topLogprobs`, `returnEmbeddings`, `priority`, `maxWaitSeconds`, `timeoutSeconds`,
323
+ `stopOnRepetition`, `defer`, `scopeId`, and `scopeRevision`. This prevents a client from attaching
323
324
  work to a decision-only call and mistakenly assuming it ran.
324
325
 
325
326
  `run_task.messages` preserves Ollama message fields beyond `role` and `content`, including images,
@@ -368,7 +369,7 @@ Scripts that bypass MCP post the same fields in snake_case to `POST /_freellama/
368
369
  `task`, `objective`, `model`, `session_id`, `required_capabilities`, `prompt` or `messages`,
369
370
  `images`, `priority`, `keep_alive`, `max_wait_seconds`, `timeout_seconds`, `stop_on_repetition`,
370
371
  and `request_options: {options, format, think, logprobs, top_logprobs}`. A batch is
371
- `{tasks: [{id, independent: true, task: {...}}], max_parallelism}`; sessions are
372
+ `POST /_freellama/v1/task-batches` with `{tasks: [{id, independent: true, task: {...}}], max_parallelism}`; sessions are
372
373
  `POST /_freellama/v1/sessions` with `{}`. `model` accepts a bare name (`name` resolves to
373
374
  `name:latest`, or the one installed tag of that family); an ambiguous or unknown name is refused
374
375
  with the installed candidates. See [Request shapes](https://github.com/bgauryy/FreeLlama/blob/main/docs/ARCHITECTURE.md#request-shapes)
package/dist/index.js CHANGED
@@ -21629,6 +21629,12 @@ function serveBinaryCandidates() {
21629
21629
  if (process.env.FREELLAMA_SERVE_BINARY) candidates.unshift(process.env.FREELLAMA_SERVE_BINARY);
21630
21630
  return candidates;
21631
21631
  }
21632
+ function startServeInBackground() {
21633
+ ensureServe(void 0).catch((error2) => {
21634
+ process.stderr.write(`[freellama serve] background start failed: ${error2 instanceof Error ? error2.message : String(error2)}
21635
+ `);
21636
+ });
21637
+ }
21632
21638
  function stopAutostartedServe() {
21633
21639
  if (child && child.exitCode === null) child.kill("SIGTERM");
21634
21640
  child = null;
@@ -22371,27 +22377,38 @@ function libraryTrialCandidate(tag, embedding) {
22371
22377
  caution: "Measure this exact tag on representative tasks before choosing it. Include resident runners, context/KV caches and OS headroom in memory fit. Discovery does not authorize a pull."
22372
22378
  };
22373
22379
  }
22380
+ var SIZE_CHIP = /^(?:\d+x)?(?:e)?\d+(?:\.\d+)?[kmbt]$/i;
22374
22381
  function parseModelSearch(html) {
22375
22382
  const results = [];
22376
- for (const block of html.split(/<li\s/).slice(1)) {
22383
+ for (const block of html.split(/<li\b/).slice(1)) {
22377
22384
  const link = block.match(/href="\/(?:library\/([\w.-]+)|([\w.-]+\/[\w.-]+))"/);
22378
22385
  const name = link?.[1] ?? link?.[2];
22379
22386
  if (!name) continue;
22380
- if (!/<p\b[^>]*class="max-w-lg/.test(block)) continue;
22381
- const rawDescription = block.match(/<p class="max-w-lg[^"]*">([\s\S]*?)<\/p>/)?.[1];
22382
- const description = rawDescription === void 0 ? "" : decodeHtml(rawDescription.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
22383
- const capabilities = [...block.matchAll(/text-(?:indigo-600|cyan-500)[^>]*>([a-z]+)<\/span>/g)].map(
22384
- (m) => m[1]
22385
- );
22386
- const stat = (label) => block.match(
22387
- new RegExp(`<span >([\\d.,KMB]+)<\\/span>\\s*<span class="hidden sm:flex">&nbsp;${label}`)
22388
- )?.[1] ?? null;
22387
+ const rawDescription = block.match(/<p\b[^>]*>([\s\S]*?)<\/p>/)?.[1];
22388
+ if (rawDescription === void 0) continue;
22389
+ const description = decodeHtml(rawDescription.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
22390
+ const chips = [...block.matchAll(/<span\b[^>]*class="[^"]*\b(?:rounded-md|text-(?:indigo|cyan|blue)-\d+)\b[^"]*"[^>]*>\s*([\w.-]+)\s*<\/span>/g)].map((m) => ({ text: m[1].toLowerCase(), at: m.index }));
22391
+ const first = chips.find((chip) => !SIZE_CHIP.test(chip.text));
22392
+ const groupStart = first ? block.lastIndexOf("<div", first.at) : -1;
22393
+ const groupEnd = groupStart >= 0 ? block.indexOf("</div>", first.at) : -1;
22394
+ const inGroup = (at) => groupStart < 0 || groupEnd < 0 || at > groupStart && at < groupEnd;
22395
+ const words = chips.filter((chip) => !SIZE_CHIP.test(chip.text) && /^[a-z]+$/.test(chip.text));
22396
+ const sizes = chips.filter((chip) => SIZE_CHIP.test(chip.text)).map((chip) => chip.text);
22397
+ const capabilities = [...new Set(words.filter((chip) => inGroup(chip.at)).map((chip) => chip.text))];
22398
+ const labels = [...new Set(words.filter((chip) => !inGroup(chip.at)).map((chip) => chip.text))];
22399
+ const legacyStat = (label) => block.match(new RegExp(`<span >([\\d.,KMB]+)<\\/span>\\s*<span class="hidden sm:flex">&nbsp;${label}`))?.[1] ?? null;
22400
+ const downloads = block.match(/title="([\d,]+) downloads"/)?.[1];
22401
+ const pullsShort = block.match(/title="[\d,]+ downloads"[\s\S]*?<span\s*>([\d.,]+[KMB]?)<\/span>/)?.[1];
22402
+ const tagCount = block.match(/>\s*(\d+)\s+tags?\s*</)?.[1];
22389
22403
  results.push({
22390
22404
  name,
22391
22405
  description: clipText(description, 160),
22392
22406
  capabilities,
22393
- pulls: stat("Pulls"),
22394
- tags: stat("Tag"),
22407
+ ...labels.length ? { labels } : {},
22408
+ ...sizes.length ? { sizes } : {},
22409
+ pulls: pullsShort ?? legacyStat("Pulls"),
22410
+ ...downloads ? { downloads: Number(downloads.replace(/,/g, "")) } : {},
22411
+ tags: tagCount ?? legacyStat("Tag"),
22395
22412
  cloudAvailable: capabilities.includes("cloud")
22396
22413
  });
22397
22414
  }
@@ -22430,6 +22447,130 @@ function parseModelTags(html, family) {
22430
22447
  return { family, tags };
22431
22448
  }
22432
22449
 
22450
+ // src/huggingface.ts
22451
+ var HUB = "https://huggingface.co";
22452
+ var MAX_JSON_BYTES = 4 * 1024 * 1024;
22453
+ function huggingFaceReference(model) {
22454
+ const match = model.match(/^(?:https?:\/\/)?(?:hf\.co|huggingface\.co)\/([\w.-]+)\/([\w.-]+?)(?::([\w.-]+))?$/i);
22455
+ if (!match || [match[1], match[2]].some((part) => part === "." || part === "..")) return null;
22456
+ const repo = `${match[1]}/${match[2]}`;
22457
+ return { repo, family: `hf.co/${repo}`, quant: match[3] ?? null, url: `${HUB}/${repo}` };
22458
+ }
22459
+ var QUANT = /^(?:UD-)?(?:I?Q\d(?:_[A-Z0-9]+)*|BF16|F16|F32|MXFP4(?:_MOE)?|TQ\d_\d)$/i;
22460
+ var SPLIT = /-\d{5}-of-\d{5}$/;
22461
+ function quantOf(path6) {
22462
+ const parts = path6.split("/");
22463
+ const stem = parts.pop().replace(/\.gguf$/i, "").replace(SPLIT, "");
22464
+ for (let index = 0; index <= stem.length; index++) {
22465
+ if (index > 0 && !/[-.]/.test(stem[index - 1])) continue;
22466
+ if (QUANT.test(stem.slice(index))) return stem.slice(index);
22467
+ }
22468
+ return parts.reverse().find((dir) => QUANT.test(dir)) ?? null;
22469
+ }
22470
+ var decimalSize = (bytes) => bytes >= 1e9 ? `${(bytes / 1e9).toFixed(1)}GB` : `${Math.round(bytes / 1e6)}MB`;
22471
+ function parseHuggingFaceFiles(tree, repo) {
22472
+ const builds = /* @__PURE__ */ new Map();
22473
+ const projectors = [];
22474
+ for (const entry of tree) {
22475
+ if (entry.type !== "file" || !entry.path || !/\.gguf$/i.test(entry.path)) continue;
22476
+ const file = entry.path.split("/").pop();
22477
+ if (/mmproj/i.test(file)) {
22478
+ projectors.push(entry.path);
22479
+ continue;
22480
+ }
22481
+ const key = entry.path.replace(/-\d{5}-of-(\d{5})\.gguf$/i, "-00001-of-$1.gguf");
22482
+ const bytes = entry.lfs?.size ?? entry.size ?? 0;
22483
+ const build = builds.get(key) ?? { path: key, file: key.split("/").pop(), quant: quantOf(entry.path), bytes: 0, files: 0 };
22484
+ builds.set(key, { ...build, bytes: build.bytes + bytes, files: build.files + 1 });
22485
+ }
22486
+ const perQuant = /* @__PURE__ */ new Map();
22487
+ for (const { quant } of builds.values()) if (quant) perQuant.set(quant.toLowerCase(), (perQuant.get(quant.toLowerCase()) ?? 0) + 1);
22488
+ const tags = [...builds.values()].sort((a, b) => a.bytes - b.bytes).map(({ path: path6, file, quant, bytes, files }) => ({
22489
+ tag: `hf.co/${repo}:${quant && perQuant.get(quant.toLowerCase()) === 1 ? quant : file}`,
22490
+ quant,
22491
+ path: path6,
22492
+ size: bytes ? decimalSize(bytes) : null,
22493
+ sizeBytes: bytes || null,
22494
+ files,
22495
+ cloud: false,
22496
+ // Hugging Face's Ollama guide shows single-file builds; say so rather than promise a pull.
22497
+ ...files > 1 ? { note: `Split into ${files} files; the Hub's Ollama guide documents single-file GGUFs, so confirm the pull.` } : {}
22498
+ }));
22499
+ return { tags, projectors };
22500
+ }
22501
+ function parseHuggingFaceSearch(models) {
22502
+ return models.filter((model) => model.id).map((model) => ({
22503
+ name: `hf.co/${model.id}`,
22504
+ source: "huggingface",
22505
+ task: model.pipeline_tag ?? null,
22506
+ downloads: model.downloads ?? null,
22507
+ likes: model.likes ?? null,
22508
+ updated: model.lastModified ?? null
22509
+ }));
22510
+ }
22511
+ var HUB_EMBEDDING_TASKS = /* @__PURE__ */ new Set(["feature-extraction", "sentence-similarity"]);
22512
+ var HuggingFaceClient = class {
22513
+ constructor(options = {}) {
22514
+ this.options = options;
22515
+ }
22516
+ options;
22517
+ async json(url) {
22518
+ const response = await (this.options.fetch ?? fetch)(url, {
22519
+ method: "GET",
22520
+ headers: { accept: "application/json" },
22521
+ signal: AbortSignal.timeout(this.options.timeoutMs ?? Math.min(DEFAULT_OLLAMA_FETCH_TIMEOUT_SECONDS * 1e3, 1e4))
22522
+ });
22523
+ if (!response.ok) {
22524
+ await response.body?.cancel();
22525
+ throw new Error(response.status === 404 || response.status === 401 ? `Hugging Face has no public model at ${url} (HTTP ${response.status}).` : `Hugging Face returned HTTP ${response.status}.`);
22526
+ }
22527
+ const text = await response.text();
22528
+ if (text.length > MAX_JSON_BYTES) throw new Error("Hugging Face response exceeds the 4 MiB limit.");
22529
+ return JSON.parse(text);
22530
+ }
22531
+ stamp() {
22532
+ return new Date((this.options.now ?? Date.now)()).toISOString();
22533
+ }
22534
+ /** GGUF repositories matching `query`, most downloaded (or newest) first. */
22535
+ async search(query, { limit = 10, order = "popular", task } = {}) {
22536
+ const params = new URLSearchParams({
22537
+ search: query,
22538
+ filter: "gguf",
22539
+ sort: order === "newest" ? "lastModified" : "downloads",
22540
+ direction: "-1",
22541
+ limit: String(limit)
22542
+ });
22543
+ for (const field of ["downloads", "likes", "pipeline_tag", "lastModified"]) params.append("expand[]", field);
22544
+ if (task) params.set("pipeline_tag", task);
22545
+ const url = `${HUB}/api/models?${params}`;
22546
+ return { url, models: parseHuggingFaceSearch(await this.json(url)), fetchedAt: this.stamp() };
22547
+ }
22548
+ /** One repo's pullable quants, task, architecture and context length. */
22549
+ async lookup(model) {
22550
+ const ref = huggingFaceReference(model);
22551
+ if (!ref) throw new Error("Use hf.co/<owner>/<repo>, the name Ollama pulls from Hugging Face.");
22552
+ const info = await this.json(
22553
+ `${HUB}/api/models/${ref.repo}?expand[]=gguf&expand[]=pipeline_tag&expand[]=downloads&expand[]=likes&expand[]=lastModified&expand[]=sha&expand[]=gated`
22554
+ );
22555
+ const tree = await this.json(`${HUB}/api/models/${ref.repo}/tree/${info.sha ?? "main"}?recursive=true`);
22556
+ return {
22557
+ ...ref,
22558
+ task: info.pipeline_tag ?? null,
22559
+ revision: info.sha ?? null,
22560
+ // "auto" or "manual": Hugging Face serves the files only after you accept the repo's terms.
22561
+ gated: info.gated && info.gated !== "false" ? info.gated : false,
22562
+ architecture: info.gguf?.architecture ?? null,
22563
+ contextLength: info.gguf?.context_length ?? null,
22564
+ parameters: info.gguf?.total ?? null,
22565
+ downloads: info.downloads ?? null,
22566
+ likes: info.likes ?? null,
22567
+ updated: info.lastModified ?? null,
22568
+ ...parseHuggingFaceFiles(tree, ref.repo),
22569
+ fetchedAt: this.stamp()
22570
+ };
22571
+ }
22572
+ };
22573
+
22433
22574
  // src/model-knowledge.ts
22434
22575
  var CACHE_TTL_MS = 60 * 60 * 1e3;
22435
22576
  var FAILURE_TTL_MS = 60 * 1e3;
@@ -22707,12 +22848,32 @@ var modelsResultSchema = external_exports.object({
22707
22848
  library: external_exports.unknown().optional()
22708
22849
  }).passthrough();
22709
22850
  var ollamaLibrary = new OllamaLibraryClient();
22851
+ var huggingFace = new HuggingFaceClient();
22710
22852
  var HOST_MEMORY_FIT_FRACTION = 0.6;
22853
+ var HUB_TASKS = /* @__PURE__ */ new Map([
22854
+ ["embedding", ["feature-extraction", "sentence-similarity"]],
22855
+ ["vision", ["image-text-to-text"]]
22856
+ ]);
22857
+ function annotateTags(tags, installed, memoryBytes, embedding) {
22858
+ const budget = memoryBytes ? memoryBytes * HOST_MEMORY_FIT_FRACTION : null;
22859
+ const local = installed ? new Set([...installed].map((name) => name.toLowerCase())) : null;
22860
+ const annotated = tags.map((t) => ({
22861
+ ...t,
22862
+ installed: local ? local.has(t.tag.toLowerCase()) : null,
22863
+ fitsInMemory: budget && t.sizeBytes ? t.sizeBytes <= budget : null,
22864
+ fitScope: "host_memory_budget_only"
22865
+ }));
22866
+ const sized = annotated.filter((t) => t.fitsInMemory === true && !t.cloud && t.sizeBytes);
22867
+ const best = budget ? sized.sort((a, b) => embedding ? (a.sizeBytes ?? 0) - (b.sizeBytes ?? 0) : (b.sizeBytes ?? 0) - (a.sizeBytes ?? 0))[0] : void 0;
22868
+ return { annotated, budget, best };
22869
+ }
22870
+ var NO_PROFILE = "No machine profile (freellama serve unreachable), so memory fit could not be checked and no tag is recommended. Start serve, or read the sizes yourself.";
22711
22871
  async function libraryLookup({
22712
22872
  model,
22713
22873
  query,
22714
22874
  capabilities,
22715
22875
  order,
22876
+ source,
22716
22877
  limit,
22717
22878
  cursor,
22718
22879
  includeReadme,
@@ -22740,6 +22901,40 @@ async function libraryLookup({
22740
22901
  return errorResult(new Error("Library step 1 has no `cursor`; it pages step-2 tags. Use `limit` for search, or pass `model`."));
22741
22902
  }
22742
22903
  try {
22904
+ if (model && huggingFaceReference(model)) {
22905
+ const hub2 = await huggingFace.lookup(model);
22906
+ const { installed: installed2, installedUnavailable: installedUnavailable2, memoryBytes } = await localState();
22907
+ const embedding = HUB_EMBEDDING_TASKS.has(hub2.task ?? "");
22908
+ const { annotated, budget, best } = annotateTags(hub2.tags, installed2, memoryBytes, embedding);
22909
+ const tagPage = pageLiveList(annotated, limit, cursor, (tag) => tag.tag);
22910
+ return structuredResult({
22911
+ family: hub2.family,
22912
+ source: "huggingface",
22913
+ sourcePage: hub2.url,
22914
+ library: {
22915
+ status: hub2.tags.length ? "available" : "no_gguf",
22916
+ fetchedAt: hub2.fetchedAt,
22917
+ task: hub2.task,
22918
+ revision: hub2.revision,
22919
+ ...hub2.gated ? { gated: hub2.gated, gatedNote: "Accept the repo's terms on Hugging Face first; its Ollama guide documents private pulls through your Ollama SSH key added to your Hugging Face account." } : {},
22920
+ architecture: hub2.architecture,
22921
+ contextLength: hub2.contextLength,
22922
+ parameters: hub2.parameters,
22923
+ downloads: hub2.downloads,
22924
+ likes: hub2.likes,
22925
+ updated: hub2.updated,
22926
+ ...hub2.projectors.length ? { visionProjectors: hub2.projectors } : {}
22927
+ },
22928
+ ...installedUnavailable2 ? { installedUnavailable: installedUnavailable2 } : {},
22929
+ machineMemoryBytes: memoryBytes,
22930
+ fitBudgetBytes: budget,
22931
+ tags: tagPage.items,
22932
+ page: { returned: tagPage.returned, total: tagPage.total, next_cursor: tagPage.next_cursor },
22933
+ pull: hub2.tags.length ? `ollama_manage {action:"pull", model:"<tag>"} after approval; Ollama picks Q4_K_M for a bare ${hub2.family} when the repo has it.` : "This repository has no GGUF files Ollama can pull.",
22934
+ recommendationUnavailable: budget ? void 0 : NO_PROFILE,
22935
+ recommendation: best ? libraryTrialCandidate(best.tag, embedding) : null
22936
+ });
22937
+ }
22743
22938
  if (model) {
22744
22939
  const ref = libraryReference(model);
22745
22940
  if (!ref) throw new Error("Use an exact Ollama family or namespace/model name, not a URL or path.");
@@ -22755,20 +22950,12 @@ async function libraryLookup({
22755
22950
  });
22756
22951
  const tags = library.tags;
22757
22952
  const { installed: installed2, installedUnavailable: installedUnavailable2, memoryBytes } = await localState();
22758
- const budget = memoryBytes ? memoryBytes * HOST_MEMORY_FIT_FRACTION : null;
22759
- const annotated = tags.map((t) => ({
22760
- ...t,
22761
- installed: installed2 ? installed2.has(t.tag) : null,
22762
- fitsInMemory: budget && t.sizeBytes ? t.sizeBytes <= budget : null,
22763
- fitScope: "host_memory_budget_only"
22764
- }));
22765
- const runnable = annotated.filter((t) => t.fitsInMemory === true && !t.cloud);
22766
22953
  const isEmbeddingFamily = library.card.capabilities.includes("embedding");
22767
- const sized = runnable.filter((t) => t.sizeBytes);
22768
- const best = budget ? isEmbeddingFamily ? sized.sort((a, b) => (a.sizeBytes ?? 0) - (b.sizeBytes ?? 0))[0] : sized.sort((a, b) => (b.sizeBytes ?? 0) - (a.sizeBytes ?? 0))[0] : void 0;
22954
+ const { annotated, budget, best } = annotateTags(tags, installed2, memoryBytes, isEmbeddingFamily);
22769
22955
  const tagPage = pageLiveList(annotated, limit, cursor, (tag) => tag.tag);
22770
22956
  return structuredResult({
22771
22957
  family,
22958
+ source: "ollama",
22772
22959
  sourcePage: ref.url,
22773
22960
  library: {
22774
22961
  status: library.status,
@@ -22784,7 +22971,7 @@ async function libraryLookup({
22784
22971
  fitBudgetBytes: budget,
22785
22972
  tags: tagPage.items,
22786
22973
  page: { returned: tagPage.returned, total: tagPage.total, next_cursor: tagPage.next_cursor },
22787
- recommendationUnavailable: budget ? void 0 : "No machine profile (freellama serve unreachable), so memory fit could not be checked and no tag is recommended. Start serve, or read the sizes yourself.",
22974
+ recommendationUnavailable: budget ? void 0 : NO_PROFILE,
22788
22975
  recommendation: best ? libraryTrialCandidate(best.tag, isEmbeddingFamily) : null
22789
22976
  });
22790
22977
  }
@@ -22792,20 +22979,44 @@ async function libraryLookup({
22792
22979
  for (const c of capabilities ?? []) params.append("c", c);
22793
22980
  if (query) params.set("q", query);
22794
22981
  if (order === "newest") params.set("o", "newest");
22795
- const search = await ollamaLibrary.search(params);
22796
- const url = search.url;
22797
- const parsed = search.models.slice(0, limit ?? 10);
22982
+ const hubTasks = (capabilities ?? []).map((c) => HUB_TASKS.get(c));
22983
+ const hubSkipped = source === "ollama" ? "source is ollama" : !query ? "the Hub is searched only with a query" : hubTasks.length > 1 || hubTasks.some((tasks) => !tasks) ? `the Hub has no filter for ${(capabilities ?? []).filter((c) => !HUB_TASKS.has(c)).join(", ") || "several capabilities at once"}` : null;
22984
+ if (source === "huggingface" && hubSkipped) throw new Error(`source "huggingface" cannot be searched: ${hubSkipped}.`);
22985
+ const searchHub = async () => {
22986
+ const results = await Promise.all((hubTasks[0] ?? [void 0]).map((task) => huggingFace.search(query, { limit: limit ?? 10, order, task })));
22987
+ const seen = /* @__PURE__ */ new Set();
22988
+ const models = results.flatMap((r) => r.models).filter((m) => !seen.has(m.name) && seen.add(m.name)).sort((a, b) => order === "newest" ? String(b.updated).localeCompare(String(a.updated)) : (b.downloads ?? 0) - (a.downloads ?? 0)).slice(0, limit ?? 10);
22989
+ return { url: results[0].url, models, fetchedAt: results[0].fetchedAt };
22990
+ };
22991
+ const [ollamaSearch, hubSearch] = await Promise.allSettled([
22992
+ source === "huggingface" ? Promise.resolve(null) : ollamaLibrary.search(params),
22993
+ hubSkipped ? Promise.resolve(null) : searchHub()
22994
+ ]);
22995
+ const tried = [source === "huggingface" ? null : ollamaSearch, hubSkipped ? null : hubSearch].filter(Boolean);
22996
+ if (tried.every((result) => result.status === "rejected")) {
22997
+ throw new Error(tried.map((result) => result.reason instanceof Error ? result.reason.message : String(result.reason)).join("; "));
22998
+ }
22999
+ const ollama = ollamaSearch.status === "fulfilled" ? ollamaSearch.value : null;
23000
+ const hub = hubSearch.status === "fulfilled" ? hubSearch.value : null;
23001
+ const ollamaRows = (ollama?.models ?? []).slice(0, limit ?? 10).map((m) => ({ ...m, source: "ollama" }));
23002
+ const hubRows = hub?.models ?? [];
22798
23003
  const { installed, installedUnavailable } = await localState();
22799
- const installedFamilies = installed ? new Set([...installed].map((n) => n.split(":")[0])) : null;
23004
+ const local = installed ? [...installed].map((n) => n.toLowerCase()) : null;
23005
+ const isInstalled = (name) => local ? local.some((n) => n.split(":")[0] === name.toLowerCase()) : null;
23006
+ const reason = (result) => result.status === "rejected" ? result.reason instanceof Error ? result.reason.message : String(result.reason) : void 0;
22800
23007
  return structuredResult({
22801
- query: url,
22802
- fetchedAt: search.fetchedAt,
22803
- ...parsed.length === 0 ? { searchUnavailable: "No result cards were recognized. The query may have no matches or Ollama's markup may have changed; inspect the source URL." } : {},
23008
+ query: ollama?.url ?? hub?.url ?? null,
23009
+ ...ollama && hub ? { hubQuery: hub.url } : {},
23010
+ fetchedAt: ollama?.fetchedAt ?? hub?.fetchedAt ?? null,
23011
+ ...ollama && ollamaRows.length === 0 ? { searchUnavailable: "No ollama.com result cards were recognized. The query may have no matches or Ollama's markup may have changed; inspect the source URL." } : {},
23012
+ ...reason(ollamaSearch) ? { ollamaUnavailable: reason(ollamaSearch) } : {},
23013
+ ...reason(hubSearch) ? { huggingFaceUnavailable: reason(hubSearch) } : {},
23014
+ ...hubSkipped && source !== "ollama" ? { huggingFaceSkipped: hubSkipped } : {},
22804
23015
  order: order ?? "popular",
22805
- count: parsed.length,
23016
+ count: ollamaRows.length + hubRows.length,
22806
23017
  ...installedUnavailable ? { installedUnavailable } : {},
22807
- nextStep: 'Not pullable yet. Call again with view:"library", model:"<name>" to get tags, sizes and memory fit.',
22808
- models: parsed.map((m) => ({ ...m, installed: installedFamilies ? installedFamilies.has(m.name) : null }))
23018
+ nextStep: 'Not pullable yet. Call again with view:"library", model:"<name>" (an Ollama family or hf.co/<owner>/<repo>) to get tags, sizes and memory fit.',
23019
+ models: [...ollamaRows, ...hubRows].map((m) => ({ ...m, installed: isInstalled(m.name) }))
22809
23020
  });
22810
23021
  } catch (error2) {
22811
23022
  return errorResult(error2);
@@ -22815,7 +23026,7 @@ function registerModelsTool(server2) {
22815
23026
  server2.registerTool(
22816
23027
  "models",
22817
23028
  {
22818
- description: "Use when: listing installed or loaded models with capabilities, policy rank and memory fit, inspecting one model, or searching ollama.com (view:library). Do not use when: running work (run_task) or pulling (ollama_manage). Returns: models; library lists families first, then one family's tags.",
23029
+ description: "Use when: listing installed or loaded models with capabilities, policy rank and memory fit, inspecting one model, or searching ollama.com and Hugging Face GGUF repos (view:library). Do not use when: running work (run_task) or pulling (ollama_manage). Returns: models; library lists families first, then one family's tags.",
22819
23030
  inputSchema: external_exports.object({
22820
23031
  view: external_exports.enum(["installed", "resident", "detail", "raw", "library"]).optional(),
22821
23032
  model: external_exports.string().min(1).optional(),
@@ -22823,8 +23034,9 @@ function registerModelsTool(server2) {
22823
23034
  includeLibrary: external_exports.boolean().optional(),
22824
23035
  includeReadme: external_exports.boolean().optional(),
22825
23036
  query: external_exports.string().min(1).optional(),
22826
- capabilities: external_exports.array(external_exports.enum(["vision", "tools", "thinking", "embedding", "cloud"])).min(1).max(5).optional(),
23037
+ capabilities: external_exports.array(external_exports.string().regex(/^[a-z][a-z-]*$/).describe("ollama.com capability word, e.g. vision, tools, thinking, embedding, audio, decision, cloud")).min(1).max(8).optional(),
22827
23038
  order: external_exports.enum(["popular", "newest"]).optional(),
23039
+ source: external_exports.enum(["ollama", "huggingface"]).optional(),
22828
23040
  limit: external_exports.number().int().positive().max(50).optional(),
22829
23041
  cursor: external_exports.string().min(1).optional(),
22830
23042
  endpoint: endpointParam,
@@ -22833,7 +23045,7 @@ function registerModelsTool(server2) {
22833
23045
  outputSchema: modelsResultSchema,
22834
23046
  annotations: { readOnlyHint: true }
22835
23047
  },
22836
- async ({ view, model, includeVerbose, includeLibrary, includeReadme, query, capabilities, order, limit, cursor, endpoint, ollamaEndpoint }) => {
23048
+ async ({ view, model, includeVerbose, includeLibrary, includeReadme, query, capabilities, order, source, limit, cursor, endpoint, ollamaEndpoint }) => {
22837
23049
  try {
22838
23050
  const selectedView = view ?? "installed";
22839
23051
  if (includeLibrary !== void 0 && selectedView !== "installed" && selectedView !== "detail") {
@@ -22842,7 +23054,7 @@ function registerModelsTool(server2) {
22842
23054
  if (includeReadme !== void 0 && !(selectedView === "library" && model || selectedView === "detail" && includeLibrary === true)) {
22843
23055
  return errorResult(new Error('`includeReadme` needs library step 2, or view "detail" with `includeLibrary:true`.'));
22844
23056
  }
22845
- if (selectedView !== "library" && [query, capabilities, order].some((value) => value !== void 0)) {
23057
+ if (selectedView !== "library" && [query, capabilities, order, source].some((value) => value !== void 0)) {
22846
23058
  return errorResult(new Error(`view "${selectedView}" does not accept library search fields.`));
22847
23059
  }
22848
23060
  if (selectedView !== "library" && selectedView !== "raw" && [limit, cursor].some((value) => value !== void 0)) {
@@ -22854,7 +23066,7 @@ function registerModelsTool(server2) {
22854
23066
  if (selectedView !== "detail" && selectedView !== "library" && model !== void 0) {
22855
23067
  return errorResult(new Error('`model` is valid only for views "detail" and "library".'));
22856
23068
  }
22857
- if (selectedView === "library" && model && [query, capabilities, order].some((value) => value !== void 0)) {
23069
+ if (selectedView === "library" && model && [query, capabilities, order, source].some((value) => value !== void 0)) {
22858
23070
  return errorResult(
22859
23071
  new Error("Library step 2 accepts only `model` plus endpoint overrides; omit step-1 search fields.")
22860
23072
  );
@@ -22917,7 +23129,7 @@ function registerModelsTool(server2) {
22917
23129
  });
22918
23130
  }
22919
23131
  case "library":
22920
- return await libraryLookup({ model, query, capabilities, order, limit, cursor, includeReadme, endpoint, ollamaEndpoint });
23132
+ return await libraryLookup({ model, query, capabilities, order, source, limit, cursor, includeReadme, endpoint, ollamaEndpoint });
22921
23133
  default: {
22922
23134
  const result = parsedResult(await listModels2(endpoint));
22923
23135
  if (!isStructuredSuccess(result)) return result;
@@ -23942,3 +24154,4 @@ registerOllamaLifecycleTools(server);
23942
24154
  registerDelegateResearchTool(server);
23943
24155
  var transport = withoutSchemaDialect(new StdioServerTransport());
23944
24156
  await server.connect(transport);
24157
+ startServeInBackground();