@v1nvn/omlx-mcp 0.14.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/README.md +61 -0
- package/dist/index.js +510 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# omlx-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that delegates work to a local [omlx](https://github.com/jundot/omlx)
|
|
4
|
+
inference server — one-shot prompts, schema-constrained extraction, and model
|
|
5
|
+
status. Free, private, unlimited; no quota, nothing leaves the machine.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @v1nvn/omlx-mcp # stdio MCP server, talks to 127.0.0.1:6659
|
|
9
|
+
omlx serve # the inference server, if it is not already up
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Install as a Claude Code plugin: `claude plugin install omlx@agentic`.
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
| Tool | Input | Returns |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `ask` | `prompt` (required), `system`, `images` (local file paths), `model`, `max_tokens` (2048), `reasoning_effort` (`low`), `temperature` | The model's answer. If reasoning consumed the whole budget, the last 2000 chars of `reasoning_content` with `reasoning_fallback: true`. |
|
|
19
|
+
| `ask_structured` | same as `ask` plus `schema` (JSON Schema, required), `schema_name` (`response`) | Parsed JSON matching the schema — sent as `response_format: {type: "json_schema"}`. |
|
|
20
|
+
| `models` | — | Installed models: loaded state, engine type, context window, output cap, size on disk. |
|
|
21
|
+
|
|
22
|
+
Every field is documented on the wire; `reasoning_effort` accepts
|
|
23
|
+
`low` / `medium` / `xhigh` — the values the model's chat template validates
|
|
24
|
+
(`xhigh` is the template default, `low` is the fast path for delegated work).
|
|
25
|
+
|
|
26
|
+
## Config
|
|
27
|
+
|
|
28
|
+
Each value resolves in order: env → `~/.omlx/settings.json` → built-in default.
|
|
29
|
+
|
|
30
|
+
| Env | Default | Purpose |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `OMLX_URL` | `http://127.0.0.1:6659` | server base URL |
|
|
33
|
+
| `OMLX_MODEL` | `Qwen3.8-27B-oQ4e-mtp` | default model for `ask` / `ask_structured` |
|
|
34
|
+
| `OMLX_TIMEOUT_MS` | `600000` | the first call after idle may load a ~17GB model for 30-60s |
|
|
35
|
+
| `OMLX_API_KEY` | — | bearer token for `/v1/*`; the server answers 401 without it |
|
|
36
|
+
| `OMLX_SETTINGS` | `~/.omlx/settings.json` | omlx settings file to fall back to |
|
|
37
|
+
|
|
38
|
+
From the settings file the server reads `server.host` + `server.port` (base URL)
|
|
39
|
+
and `auth.api_key` — the same credential `omlx serve` issues, so a standard
|
|
40
|
+
install configures this client without any env vars.
|
|
41
|
+
|
|
42
|
+
## Boundaries
|
|
43
|
+
|
|
44
|
+
- Loopback only: requests go to `OMLX_URL` and nowhere else.
|
|
45
|
+
- `/v1/*` and `/health` only — the admin API (`/api/*`, `/admin/*`) mutates
|
|
46
|
+
server state and carries the auth secret.
|
|
47
|
+
- Thin pass-through: no prompt templating, no retries, no response
|
|
48
|
+
post-processing beyond the empty-answer fallback. Errors carry the remedy —
|
|
49
|
+
server down reads `omlx unreachable at <url> — start it with: omlx serve`.
|
|
50
|
+
- No `load`/`unload`: the server LRU-manages its model pool; an agent evicting
|
|
51
|
+
a model mid-batch is a footgun.
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
yarn install
|
|
57
|
+
yarn test # unit tests, fetch mocked
|
|
58
|
+
yarn test:live # end-to-end against the real server (RUN_LIVE=1)
|
|
59
|
+
yarn dev # hot-reloading MCP server, wired as omlx-dev
|
|
60
|
+
yarn typecheck && yarn lint:fix
|
|
61
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
var package_default = {
|
|
9
|
+
name: "@v1nvn/omlx-mcp",
|
|
10
|
+
version: "0.14.0",
|
|
11
|
+
description: "MCP server exposing a local omlx inference server (chat, structured output, model status) so coding agents can delegate bulk work to a local model.",
|
|
12
|
+
type: "module",
|
|
13
|
+
main: "dist/index.js",
|
|
14
|
+
bin: "dist/index.js",
|
|
15
|
+
scripts: {
|
|
16
|
+
"build": "vite build",
|
|
17
|
+
"dev": "vite-node src/dev.ts",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:live": "RUN_LIVE=1 vitest run test/live.test.ts"
|
|
22
|
+
},
|
|
23
|
+
keywords: [
|
|
24
|
+
"mcp",
|
|
25
|
+
"omlx",
|
|
26
|
+
"mlx",
|
|
27
|
+
"local-llm",
|
|
28
|
+
"model-context-protocol"
|
|
29
|
+
],
|
|
30
|
+
author: "v1nvn",
|
|
31
|
+
license: "MIT",
|
|
32
|
+
repository: {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/v1nvn/agentic.git",
|
|
35
|
+
"directory": "packages/omlx-mcp"
|
|
36
|
+
},
|
|
37
|
+
files: ["dist"],
|
|
38
|
+
publishConfig: { "access": "public" },
|
|
39
|
+
engines: { "node": ">=22" },
|
|
40
|
+
dependencies: {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
|
+
},
|
|
44
|
+
devDependencies: {
|
|
45
|
+
"vite": "^8.1.4",
|
|
46
|
+
"vite-node": "^6.0.0",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/logger.ts
|
|
52
|
+
var Logger = class {
|
|
53
|
+
stderr;
|
|
54
|
+
constructor(stderr = (line) => process.stderr.write(`${line}\n`)) {
|
|
55
|
+
this.stderr = stderr;
|
|
56
|
+
}
|
|
57
|
+
error(message) {
|
|
58
|
+
this.stderr(`ERROR ${message}`);
|
|
59
|
+
}
|
|
60
|
+
info(message) {
|
|
61
|
+
this.stderr(`INFO ${message}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var logger = new Logger();
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/config.ts
|
|
67
|
+
var DEFAULT_SETTINGS_PATH = join(homedir(), ".omlx", "settings.json");
|
|
68
|
+
var DEFAULT_URL = "http://127.0.0.1:6659";
|
|
69
|
+
var DEFAULT_MODEL = "Qwen3.8-27B-oQ4e-mtp";
|
|
70
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
71
|
+
var SERVER_TITLE = "omlx MCP";
|
|
72
|
+
var SERVER_DESCRIPTION = "Delegate work to a local omlx inference server on this machine — one-shot prompts, schema-constrained extraction, and installed-model status. Loopback only: the server talks to OMLX_URL (default http://127.0.0.1:6659) and nothing else; /v1 and /health only, never the admin API.";
|
|
73
|
+
var SERVER_INSTRUCTIONS = `Delegate coding-agent busywork to the local model — free, private, unlimited, no quota. Default model Qwen3.8-27B-oQ4e-mtp: ~28 tok/s with MTP speculative decoding, 256K context window, 32768 max output tokens per call. The first call after idle may block ~30-60s while the 17GB model loads.
|
|
74
|
+
|
|
75
|
+
- ask: one-shot prompt to the local model. Route here instead of answering yourself when the job is high-volume or low-stakes: commit messages, docstrings across a package, changelogs, log/diff/transcript summarization, drafts and rewrites, describing images or screenshots (local file paths in \`images\`; bounding boxes on a 0-1000 scale work well), and anything containing content that should not leave this machine.
|
|
76
|
+
- ask_structured: the same, plus a JSON \`schema\` the output must match — entities from logs, frontmatter, tables from prose, bbox JSON. Prefer this over asking \`ask\` for JSON in prose.
|
|
77
|
+
- models: installed models with loaded state, context window, and size on disk — check here for valid \`model\` ids.
|
|
78
|
+
|
|
79
|
+
Do NOT route hard reasoning, planning, or multi-file refactors to the local model — quality is a step down from cloud models. If the server is down, the tools say so with the start command; do the work yourself rather than retrying.`;
|
|
80
|
+
function asText(value) {
|
|
81
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
82
|
+
}
|
|
83
|
+
function asPort(value) {
|
|
84
|
+
const port = typeof value === "number" ? value : Number.parseInt(asText(value) ?? "", 10);
|
|
85
|
+
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
86
|
+
}
|
|
87
|
+
function baseUrlFrom(server) {
|
|
88
|
+
const host = asText(server?.host);
|
|
89
|
+
const port = asPort(server?.port);
|
|
90
|
+
if (host === void 0 || port === void 0) return;
|
|
91
|
+
return `http://${host}:${port}`;
|
|
92
|
+
}
|
|
93
|
+
function readSettings(path) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = readFileSync(path, "utf8");
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (err.code !== "ENOENT") logger.error(`unreadable settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(raw);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
logger.error(`unparseable settings at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function parseTimeoutMs(raw) {
|
|
109
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
110
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
|
|
111
|
+
}
|
|
112
|
+
function loadConfig(env = process.env) {
|
|
113
|
+
const settings = readSettings(env.OMLX_SETTINGS ?? DEFAULT_SETTINGS_PATH);
|
|
114
|
+
return {
|
|
115
|
+
apiKey: env.OMLX_API_KEY ?? asText(settings?.auth?.api_key),
|
|
116
|
+
url: (env.OMLX_URL ?? baseUrlFrom(settings?.server) ?? DEFAULT_URL).replace(/\/+$/, ""),
|
|
117
|
+
model: env.OMLX_MODEL ?? DEFAULT_MODEL,
|
|
118
|
+
timeoutMs: parseTimeoutMs(env.OMLX_TIMEOUT_MS),
|
|
119
|
+
name: "omlx-mcp",
|
|
120
|
+
version: package_default.version,
|
|
121
|
+
title: SERVER_TITLE,
|
|
122
|
+
description: SERVER_DESCRIPTION,
|
|
123
|
+
instructions: SERVER_INSTRUCTIONS
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/errors.ts
|
|
128
|
+
var OmlxError = class extends Error {
|
|
129
|
+
cause;
|
|
130
|
+
constructor(message, options = {}) {
|
|
131
|
+
super(message);
|
|
132
|
+
this.name = "OmlxError";
|
|
133
|
+
this.cause = options.cause;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function describeError(err) {
|
|
137
|
+
if (err instanceof Error) return err.message;
|
|
138
|
+
return String(err);
|
|
139
|
+
}
|
|
140
|
+
function toErrorResult(err) {
|
|
141
|
+
if (err instanceof OmlxError) return {
|
|
142
|
+
isError: true,
|
|
143
|
+
content: [{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: err.message
|
|
146
|
+
}]
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
isError: true,
|
|
150
|
+
content: [{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: `${err instanceof Error ? err.name : "Error"}: ${describeError(err)}`
|
|
153
|
+
}]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/omlx.ts
|
|
158
|
+
async function parseDetail(response) {
|
|
159
|
+
let body = {};
|
|
160
|
+
try {
|
|
161
|
+
body = await response.json();
|
|
162
|
+
} catch {}
|
|
163
|
+
if (typeof body.error?.message === "string") return body.error.message;
|
|
164
|
+
if (typeof body.detail === "string") return body.detail;
|
|
165
|
+
if (body.detail !== void 0) return JSON.stringify(body.detail);
|
|
166
|
+
return response.statusText;
|
|
167
|
+
}
|
|
168
|
+
function networkCause(err) {
|
|
169
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
170
|
+
if (cause instanceof Error && "code" in cause) return String(cause.code);
|
|
171
|
+
}
|
|
172
|
+
async function request(config, path, method, body) {
|
|
173
|
+
let response;
|
|
174
|
+
const headers = { "Content-Type": "application/json" };
|
|
175
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
176
|
+
try {
|
|
177
|
+
response = await fetch(`${config.url}${path}`, {
|
|
178
|
+
method,
|
|
179
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
180
|
+
signal: AbortSignal.timeout(config.timeoutMs),
|
|
181
|
+
headers
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err instanceof Error && err.name === "TimeoutError") throw new OmlxError(`omlx did not respond within ${config.timeoutMs}ms at ${config.url}${path} — the model may still be loading; retry, or raise OMLX_TIMEOUT_MS`, { cause: err });
|
|
185
|
+
if (err instanceof TypeError) {
|
|
186
|
+
const code = networkCause(err);
|
|
187
|
+
const at = code ? ` (${code})` : "";
|
|
188
|
+
throw new OmlxError(`omlx unreachable at ${config.url}${at} — start it with: omlx serve`, { cause: err });
|
|
189
|
+
}
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
if (response.status === 401) throw new OmlxError(`omlx requires an API key for ${path} (401: ${await parseDetail(response)}) — set OMLX_API_KEY, or auth.api_key in ~/.omlx/settings.json`);
|
|
194
|
+
throw new OmlxError(`omlx returned ${response.status} for ${path}: ${await parseDetail(response)}`);
|
|
195
|
+
}
|
|
196
|
+
return await response.json();
|
|
197
|
+
}
|
|
198
|
+
function omlxGet(config, path) {
|
|
199
|
+
return request(config, path, "GET");
|
|
200
|
+
}
|
|
201
|
+
function omlxPost(config, path, body) {
|
|
202
|
+
return request(config, path, "POST", body);
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/tools/chat.ts
|
|
206
|
+
var MIME_SIGNATURES = [
|
|
207
|
+
[[
|
|
208
|
+
137,
|
|
209
|
+
80,
|
|
210
|
+
78,
|
|
211
|
+
71
|
|
212
|
+
], "image/png"],
|
|
213
|
+
[[
|
|
214
|
+
255,
|
|
215
|
+
216,
|
|
216
|
+
255
|
|
217
|
+
], "image/jpeg"],
|
|
218
|
+
[[
|
|
219
|
+
71,
|
|
220
|
+
73,
|
|
221
|
+
70,
|
|
222
|
+
56
|
|
223
|
+
], "image/gif"],
|
|
224
|
+
[[
|
|
225
|
+
82,
|
|
226
|
+
73,
|
|
227
|
+
70,
|
|
228
|
+
70
|
|
229
|
+
], "image/webp"]
|
|
230
|
+
];
|
|
231
|
+
function detectMime(bytes, path) {
|
|
232
|
+
const mime = MIME_SIGNATURES.find(([magic]) => magic.every((byte, i) => bytes[i] === byte))?.[1];
|
|
233
|
+
if (!mime) throw new OmlxError(`unsupported image format at ${path} — expected png, jpeg, webp, or gif`);
|
|
234
|
+
if (mime === "image/webp" && bytes.subarray(8, 12).toString("ascii") !== "WEBP") throw new OmlxError(`unsupported image format at ${path} — expected png, jpeg, webp, or gif`);
|
|
235
|
+
return mime;
|
|
236
|
+
}
|
|
237
|
+
function readImagePart(path) {
|
|
238
|
+
const bytes = readFileSync(path);
|
|
239
|
+
return {
|
|
240
|
+
image_url: { url: `data:${detectMime(bytes, path)};base64,${bytes.toString("base64")}` },
|
|
241
|
+
type: "image_url"
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function buildUserContent(prompt, images) {
|
|
245
|
+
if (!images || images.length === 0) return prompt;
|
|
246
|
+
return [{
|
|
247
|
+
text: prompt,
|
|
248
|
+
type: "text"
|
|
249
|
+
}, ...images.map((path) => readImagePart(path))];
|
|
250
|
+
}
|
|
251
|
+
function buildChatCompletionRequest(config, input, responseFormat) {
|
|
252
|
+
const messages = [];
|
|
253
|
+
if (input.system) messages.push({
|
|
254
|
+
content: input.system,
|
|
255
|
+
role: "system"
|
|
256
|
+
});
|
|
257
|
+
messages.push({
|
|
258
|
+
content: buildUserContent(input.prompt, input.images),
|
|
259
|
+
role: "user"
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
max_tokens: input.max_tokens,
|
|
263
|
+
messages,
|
|
264
|
+
model: input.model ?? config.model,
|
|
265
|
+
reasoning_effort: input.reasoning_effort,
|
|
266
|
+
response_format: responseFormat,
|
|
267
|
+
temperature: input.temperature
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function extractCompletion(response) {
|
|
271
|
+
const message = response.choices?.[0]?.message;
|
|
272
|
+
const content = message?.content?.trim();
|
|
273
|
+
if (content) return {
|
|
274
|
+
reasoningFallback: false,
|
|
275
|
+
text: content
|
|
276
|
+
};
|
|
277
|
+
const reasoning = message?.reasoning_content;
|
|
278
|
+
if (!reasoning) throw new OmlxError("omlx returned an empty completion — no content and no reasoning_content; retry with a higher max_tokens");
|
|
279
|
+
return {
|
|
280
|
+
reasoningFallback: true,
|
|
281
|
+
text: reasoning.slice(-2e3)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/tools/ask.ts
|
|
286
|
+
var askInputShape = {
|
|
287
|
+
images: z.array(z.string()).describe("Local file paths of images to include (png, jpeg, webp, gif); read from disk and sent as base64 data URIs. Qwen3.8 is a VLM — asking for bounding boxes on a 0-1000 scale works well.").optional(),
|
|
288
|
+
max_tokens: z.number().int().positive().describe("Maximum completion tokens. The server caps a single generation at 32768.").default(2048),
|
|
289
|
+
model: z.string().describe("Model id on the omlx server; omit for the OMLX_MODEL default. See the `models` tool for installed ids.").optional(),
|
|
290
|
+
prompt: z.string().describe("The prompt. Keep it bounded — this is delegation work, not a whole task."),
|
|
291
|
+
reasoning_effort: z.enum([
|
|
292
|
+
"low",
|
|
293
|
+
"medium",
|
|
294
|
+
"xhigh"
|
|
295
|
+
]).describe("Reasoning depth. 'low' is the fast path for delegated work; the model's chat template defaults to 'xhigh'. Use 'medium' when a single bad answer costs a retry.").default("low"),
|
|
296
|
+
system: z.string().describe("Optional system message.").optional(),
|
|
297
|
+
temperature: z.number().min(0).describe("Omit to use the server default (1.0).").optional()
|
|
298
|
+
};
|
|
299
|
+
var askInputSchema = z.object(askInputShape);
|
|
300
|
+
var askOutputSchema = {
|
|
301
|
+
answer: z.string().describe("The model's answer."),
|
|
302
|
+
model: z.string().describe("The model id that produced the answer."),
|
|
303
|
+
reasoning_fallback: z.boolean().describe("True when the model spent the whole token budget on reasoning and produced no answer content — `answer` is then the last 2000 chars of reasoning_content.")
|
|
304
|
+
};
|
|
305
|
+
var ASK_TOOL_DESCRIPTION = `Ask the local Qwen3.8-27B on this Mac — free, private, unlimited, ~28 tok/s, 256K context. Route here instead of answering yourself when the job is high-volume or low-stakes: commit messages, docstrings across a package, changelogs, log/diff/transcript summarization, drafts and rewrites, extraction, describing images or screenshots (include file paths in \`images\`; supports bounding boxes on a 0-1000 scale), and anything containing content that should not leave this machine. Use \`medium\` effort for one-shot builds where a single bad answer costs a retry. Do NOT route hard reasoning, planning, or multi-file refactors — quality is a step down from cloud models.`;
|
|
306
|
+
async function runAsk(rawArgs) {
|
|
307
|
+
const config = loadConfig();
|
|
308
|
+
const completion = await chatCompletion(config, buildChatCompletionRequest(config, askInputSchema.parse(rawArgs)));
|
|
309
|
+
return {
|
|
310
|
+
content: [{
|
|
311
|
+
text: completion.answer,
|
|
312
|
+
type: "text"
|
|
313
|
+
}],
|
|
314
|
+
structuredContent: completion
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function chatCompletion(config, request) {
|
|
318
|
+
return omlxPost(config, "/v1/chat/completions", request).then((response) => {
|
|
319
|
+
const completion = extractCompletion(response);
|
|
320
|
+
return {
|
|
321
|
+
answer: completion.text,
|
|
322
|
+
model: request.model,
|
|
323
|
+
reasoning_fallback: completion.reasoningFallback
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function askHandler(args) {
|
|
328
|
+
return runAsk(args).catch((err) => {
|
|
329
|
+
logger.error(`ask failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
330
|
+
return toErrorResult(err);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function registerAskTool(server) {
|
|
334
|
+
return server.registerTool("ask", {
|
|
335
|
+
title: "Ask the local model",
|
|
336
|
+
description: ASK_TOOL_DESCRIPTION,
|
|
337
|
+
inputSchema: askInputShape,
|
|
338
|
+
outputSchema: askOutputSchema
|
|
339
|
+
}, askHandler);
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/tools/ask_structured.ts
|
|
343
|
+
var askStructuredInputShape = {
|
|
344
|
+
...askInputShape,
|
|
345
|
+
schema: z.record(z.string(), z.unknown()).describe("JSON Schema (draft-agnostic object) the output must match — e.g. {\"type\":\"object\",\"properties\":{\"files\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"files\"]}. Top-level arrays and scalars work too."),
|
|
346
|
+
schema_name: z.string().describe("Wire metadata name for the schema.").default("response")
|
|
347
|
+
};
|
|
348
|
+
var askStructuredInputSchema = z.object(askStructuredInputShape);
|
|
349
|
+
var askStructuredOutputSchema = {
|
|
350
|
+
model: z.string().describe("The model id that produced the result."),
|
|
351
|
+
reasoning_fallback: z.boolean().describe("True when the model produced no content — `result` is then unparsed reasoning text."),
|
|
352
|
+
result: z.unknown().describe("The parsed JSON value returned by the model.")
|
|
353
|
+
};
|
|
354
|
+
var ASK_STRUCTURED_TOOL_DESCRIPTION = `Ask the local Qwen3.8-27B on this Mac with schema-constrained output — the response must match the \`schema\` JSON Schema, returned as parsed JSON, not prose. Free, private, unlimited, ~28 tok/s, 256K context. Prefer this over asking \`ask\` for JSON whenever the shape is known: entities from logs, frontmatter, tables from prose, bbox JSON from a screenshot (give file paths in \`images\`), classification tags. Keep schemas shallow — deep nesting and long enums tax a 27B model. Do NOT route hard reasoning or multi-file analysis here — quality is a step down from cloud models.`;
|
|
355
|
+
async function runAskStructured(rawArgs) {
|
|
356
|
+
const config = loadConfig();
|
|
357
|
+
const input = askStructuredInputSchema.parse(rawArgs);
|
|
358
|
+
const completion = await chatCompletion(config, buildChatCompletionRequest(config, input, {
|
|
359
|
+
json_schema: {
|
|
360
|
+
name: input.schema_name,
|
|
361
|
+
schema: input.schema
|
|
362
|
+
},
|
|
363
|
+
type: "json_schema"
|
|
364
|
+
}));
|
|
365
|
+
let parsed;
|
|
366
|
+
try {
|
|
367
|
+
parsed = JSON.parse(completion.answer);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
throw new OmlxError(`omlx returned non-JSON despite json_schema — first 200 chars: ${completion.answer.slice(0, 200)}; retry, or loosen the schema`, { cause: err });
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
content: [{
|
|
373
|
+
text: JSON.stringify(parsed, null, 2),
|
|
374
|
+
type: "text"
|
|
375
|
+
}],
|
|
376
|
+
structuredContent: {
|
|
377
|
+
model: completion.model,
|
|
378
|
+
reasoning_fallback: completion.reasoning_fallback,
|
|
379
|
+
result: parsed
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function askStructuredHandler(args) {
|
|
384
|
+
return runAskStructured(args).catch((err) => {
|
|
385
|
+
logger.error(`ask_structured failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
386
|
+
return toErrorResult(err);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function registerAskStructuredTool(server) {
|
|
390
|
+
return server.registerTool("ask_structured", {
|
|
391
|
+
title: "Ask the local model for structured output",
|
|
392
|
+
description: ASK_STRUCTURED_TOOL_DESCRIPTION,
|
|
393
|
+
inputSchema: askStructuredInputShape,
|
|
394
|
+
outputSchema: askStructuredOutputSchema
|
|
395
|
+
}, askStructuredHandler);
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/tools/models.ts
|
|
399
|
+
var modelsOutputSchema = {
|
|
400
|
+
loaded_count: z.number().int().describe("How many models are currently in memory."),
|
|
401
|
+
model_count: z.number().int().describe("How many models are installed on disk."),
|
|
402
|
+
models: z.array(z.object({
|
|
403
|
+
context_window: z.number().int().nullable().describe("Maximum context window in tokens."),
|
|
404
|
+
engine_type: z.string().nullable().describe("Engine class, e.g. \"vlm\" for vision-language models."),
|
|
405
|
+
id: z.string().describe("Model id — the `model` argument for ask/ask_structured."),
|
|
406
|
+
loaded: z.boolean().describe("Whether the model is resident in memory."),
|
|
407
|
+
loading: z.boolean().describe("Whether the model is loading right now."),
|
|
408
|
+
max_output_tokens: z.number().int().nullable().describe("Maximum output tokens for a single generation."),
|
|
409
|
+
size_bytes: z.number().int().nullable().describe("Model size on disk in bytes.")
|
|
410
|
+
})).describe("Installed models, sorted by id.")
|
|
411
|
+
};
|
|
412
|
+
var MODELS_TOOL_DESCRIPTION = `List the models installed on the local omlx server with loaded state, context window, output cap, and size on disk — the source of truth for the \`model\` argument of \`ask\`/\`ask_structured\`. Read-only: load/unload stay out on purpose, the server LRU-manages the model pool itself.`;
|
|
413
|
+
function summarize(status) {
|
|
414
|
+
return {
|
|
415
|
+
context_window: status.max_context_window ?? null,
|
|
416
|
+
engine_type: status.engine_type ?? null,
|
|
417
|
+
id: status.id ?? "",
|
|
418
|
+
loaded: status.loaded ?? false,
|
|
419
|
+
loading: status.is_loading ?? false,
|
|
420
|
+
max_output_tokens: status.max_tokens ?? null,
|
|
421
|
+
size_bytes: status.actual_size ?? status.estimated_size ?? null
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function formatBytes(bytes) {
|
|
425
|
+
if (bytes === null) return "size ?";
|
|
426
|
+
return `size ${(bytes / 1e9).toFixed(1)} GB`;
|
|
427
|
+
}
|
|
428
|
+
function renderModels(models) {
|
|
429
|
+
return models.map((model) => `${model.id} ${model.loading ? "loading" : model.loaded ? "loaded" : "unloaded"} ${model.engine_type ?? "engine ?"} context ${model.context_window ?? "?"} out ${model.max_output_tokens ?? "?"} ${formatBytes(model.size_bytes)}`).join("\n");
|
|
430
|
+
}
|
|
431
|
+
async function runModels() {
|
|
432
|
+
const config = loadConfig();
|
|
433
|
+
const [list, status] = await Promise.all([omlxGet(config, "/v1/models"), omlxGet(config, "/v1/models/status")]);
|
|
434
|
+
const models = (status.models ?? []).map(summarize).sort((a, b) => a.id.localeCompare(b.id));
|
|
435
|
+
if (models.length === 0 && (list.data ?? []).length > 0) {
|
|
436
|
+
for (const entry of list.data ?? []) if (entry.id && !models.some((model) => model.id === entry.id)) models.push({
|
|
437
|
+
context_window: null,
|
|
438
|
+
engine_type: null,
|
|
439
|
+
id: entry.id,
|
|
440
|
+
loaded: false,
|
|
441
|
+
loading: false,
|
|
442
|
+
max_output_tokens: null,
|
|
443
|
+
size_bytes: null
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
content: [{
|
|
448
|
+
text: renderModels(models),
|
|
449
|
+
type: "text"
|
|
450
|
+
}],
|
|
451
|
+
structuredContent: {
|
|
452
|
+
loaded_count: status.loaded_count ?? 0,
|
|
453
|
+
model_count: status.model_count ?? models.length,
|
|
454
|
+
models
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function modelsHandler() {
|
|
459
|
+
return runModels().catch((err) => {
|
|
460
|
+
logger.error(`models failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
461
|
+
return toErrorResult(err);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function registerModelsTool(server) {
|
|
465
|
+
return server.registerTool("models", {
|
|
466
|
+
title: "List installed local models",
|
|
467
|
+
description: MODELS_TOOL_DESCRIPTION,
|
|
468
|
+
inputSchema: {},
|
|
469
|
+
outputSchema: modelsOutputSchema
|
|
470
|
+
}, modelsHandler);
|
|
471
|
+
}
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/server.ts
|
|
474
|
+
function createMcpServer() {
|
|
475
|
+
const { name, version, title, description, instructions } = loadConfig();
|
|
476
|
+
return new McpServer({
|
|
477
|
+
name,
|
|
478
|
+
version,
|
|
479
|
+
title,
|
|
480
|
+
description
|
|
481
|
+
}, { instructions });
|
|
482
|
+
}
|
|
483
|
+
function registerTools(server) {
|
|
484
|
+
return [
|
|
485
|
+
registerAskStructuredTool(server),
|
|
486
|
+
registerAskTool(server),
|
|
487
|
+
registerModelsTool(server)
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
function createServer() {
|
|
491
|
+
const server = createMcpServer();
|
|
492
|
+
registerTools(server);
|
|
493
|
+
return server;
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/index.ts
|
|
497
|
+
var server = createServer();
|
|
498
|
+
var transport = new StdioServerTransport();
|
|
499
|
+
await server.connect(transport);
|
|
500
|
+
function shutdown() {
|
|
501
|
+
server.close().catch(() => {}).finally(() => {
|
|
502
|
+
process.exit(0);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
process.on("SIGINT", shutdown);
|
|
506
|
+
process.on("SIGTERM", shutdown);
|
|
507
|
+
//#endregion
|
|
508
|
+
export {};
|
|
509
|
+
|
|
510
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../package.json","../src/logger.ts","../src/config.ts","../src/errors.ts","../src/omlx.ts","../src/tools/chat.ts","../src/tools/ask.ts","../src/tools/ask_structured.ts","../src/tools/models.ts","../src/server.ts","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@v1nvn/omlx-mcp\",\n \"version\": \"0.14.0\",\n \"description\": \"MCP server exposing a local omlx inference server (chat, structured output, model status) so coding agents can delegate bulk work to a local model.\",\n \"type\": \"module\",\n \"main\": \"dist/index.js\",\n \"bin\": \"dist/index.js\",\n \"scripts\": {\n \"build\": \"vite build\",\n \"dev\": \"vite-node src/dev.ts\",\n \"start\": \"node dist/index.js\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:live\": \"RUN_LIVE=1 vitest run test/live.test.ts\"\n },\n \"keywords\": [\n \"mcp\",\n \"omlx\",\n \"mlx\",\n \"local-llm\",\n \"model-context-protocol\"\n ],\n \"author\": \"v1nvn\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/v1nvn/agentic.git\",\n \"directory\": \"packages/omlx-mcp\"\n },\n \"files\": [\n \"dist\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.29.0\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"vite\": \"^8.1.4\",\n \"vite-node\": \"^6.0.0\",\n \"vitest\": \"^4.1.10\"\n }\n}\n","type Writer = (message: string) => void;\n\n// stdout carries the MCP transport; everything here goes to stderr.\nclass Logger {\n private readonly stderr: Writer;\n\n constructor(stderr: Writer = line => process.stderr.write(`${line}\\n`)) {\n this.stderr = stderr;\n }\n\n error(message: string): void {\n this.stderr(`ERROR ${message}`);\n }\n\n info(message: string): void {\n this.stderr(`INFO ${message}`);\n }\n}\n\nexport const logger = new Logger();\n","import { readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport pkg from '../package.json' with { type: 'json' };\nimport { logger } from './logger.js';\n\nexport interface OmlxConfig {\n readonly apiKey: string | undefined;\n readonly model: string;\n readonly timeoutMs: number;\n readonly url: string;\n}\n\nexport interface ServerConfig extends OmlxConfig {\n readonly description: string;\n readonly instructions: string;\n readonly name: 'omlx-mcp';\n readonly title: string;\n readonly version: string;\n}\n\n// The omlx server's own settings file (~/.omlx/settings.json). Its `server` and\n// `auth` sections describe the listening socket and the API key it issues, so\n// they configure this client for free — the rest of the file is irrelevant here.\ninterface OmlxSettings {\n auth?: { api_key?: unknown };\n server?: { host?: unknown; port?: unknown };\n}\n\nconst DEFAULT_SETTINGS_PATH = join(homedir(), '.omlx', 'settings.json');\nconst DEFAULT_URL = 'http://127.0.0.1:6659';\nconst DEFAULT_MODEL = 'Qwen3.8-27B-oQ4e-mtp';\nconst DEFAULT_TIMEOUT_MS = 600_000;\n\nconst SERVER_TITLE = 'omlx MCP';\n\nconst SERVER_DESCRIPTION =\n 'Delegate work to a local omlx inference server on this machine — one-shot prompts, schema-constrained extraction, and installed-model status. Loopback only: the server talks to OMLX_URL (default http://127.0.0.1:6659) and nothing else; /v1 and /health only, never the admin API.';\n\nconst SERVER_INSTRUCTIONS = `Delegate coding-agent busywork to the local model — free, private, unlimited, no quota. Default model Qwen3.8-27B-oQ4e-mtp: ~28 tok/s with MTP speculative decoding, 256K context window, 32768 max output tokens per call. The first call after idle may block ~30-60s while the 17GB model loads.\n\n- ask: one-shot prompt to the local model. Route here instead of answering yourself when the job is high-volume or low-stakes: commit messages, docstrings across a package, changelogs, log/diff/transcript summarization, drafts and rewrites, describing images or screenshots (local file paths in \\`images\\`; bounding boxes on a 0-1000 scale work well), and anything containing content that should not leave this machine.\n- ask_structured: the same, plus a JSON \\`schema\\` the output must match — entities from logs, frontmatter, tables from prose, bbox JSON. Prefer this over asking \\`ask\\` for JSON in prose.\n- models: installed models with loaded state, context window, and size on disk — check here for valid \\`model\\` ids.\n\nDo NOT route hard reasoning, planning, or multi-file refactors to the local model — quality is a step down from cloud models. If the server is down, the tools say so with the start command; do the work yourself rather than retrying.`;\n\nfunction asText(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() !== ''\n ? value.trim()\n : undefined;\n}\n\nfunction asPort(value: unknown): number | undefined {\n const port =\n typeof value === 'number'\n ? value\n : Number.parseInt(asText(value) ?? '', 10);\n return Number.isInteger(port) && port > 0 ? port : undefined;\n}\n\nfunction baseUrlFrom(server: OmlxSettings['server']): string | undefined {\n const host = asText(server?.host);\n const port = asPort(server?.port);\n if (host === undefined || port === undefined) {\n return undefined;\n }\n return `http://${host}:${port}`;\n}\n\nfunction readSettings(path: string): OmlxSettings | undefined {\n let raw: string;\n try {\n raw = readFileSync(path, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n logger.error(\n `unreadable settings at ${path}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n return undefined;\n }\n try {\n return JSON.parse(raw) as OmlxSettings;\n } catch (err) {\n logger.error(\n `unparseable settings at ${path}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return undefined;\n }\n}\n\nfunction parseTimeoutMs(raw: string | undefined): number {\n const parsed = Number.parseInt(raw ?? '', 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {\n const settings = readSettings(env.OMLX_SETTINGS ?? DEFAULT_SETTINGS_PATH);\n return {\n apiKey: env.OMLX_API_KEY ?? asText(settings?.auth?.api_key),\n // A trailing slash would double up in path joins (\"/v1/\" + \"/v1/models\").\n url: (env.OMLX_URL ?? baseUrlFrom(settings?.server) ?? DEFAULT_URL).replace(\n /\\/+$/,\n '',\n ),\n model: env.OMLX_MODEL ?? DEFAULT_MODEL,\n timeoutMs: parseTimeoutMs(env.OMLX_TIMEOUT_MS),\n name: 'omlx-mcp',\n version: pkg.version,\n title: SERVER_TITLE,\n description: SERVER_DESCRIPTION,\n instructions: SERVER_INSTRUCTIONS,\n };\n}\n","import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nexport class OmlxError extends Error {\n public override readonly cause: unknown;\n\n public constructor(message: string, options: { cause?: unknown } = {}) {\n super(message);\n this.name = 'OmlxError';\n this.cause = options.cause;\n }\n}\n\nfunction describeError(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n\nexport function toErrorResult(err: unknown): CallToolResult {\n if (err instanceof OmlxError) {\n // Messages already carry the remedy (\"unreachable at <url> — start it\n // with: omlx serve\"); a class-name prefix would only repeat them.\n return {\n isError: true,\n content: [{ type: 'text', text: err.message }],\n };\n }\n const label = err instanceof Error ? err.name : 'Error';\n return {\n isError: true,\n content: [{ type: 'text', text: `${label}: ${describeError(err)}` }],\n };\n}\n","import type { OmlxConfig } from './config.js';\n\nimport { OmlxError } from './errors.js';\n\ninterface ErrorBody {\n detail?: unknown;\n error?: { message?: unknown };\n}\n\nasync function parseDetail(response: Response): Promise<string> {\n let body: ErrorBody = {};\n try {\n body = (await response.json()) as ErrorBody;\n } catch {\n // Non-JSON error body; fall through to status text.\n }\n // /v1 endpoints answer with the OpenAI error shape; the admin API with detail.\n if (typeof body.error?.message === 'string') {\n return body.error.message;\n }\n if (typeof body.detail === 'string') {\n return body.detail;\n }\n if (body.detail !== undefined) {\n // FastAPI validation errors are a list of {loc, msg, type}.\n return JSON.stringify(body.detail);\n }\n return response.statusText;\n}\n\nfunction networkCause(err: unknown): string | undefined {\n const cause = err instanceof Error ? err.cause : undefined;\n if (cause instanceof Error && 'code' in cause) {\n return String(cause.code);\n }\n return undefined;\n}\n\nasync function request<T>(\n config: OmlxConfig,\n path: string,\n method: 'GET' | 'POST',\n body?: unknown,\n): Promise<T> {\n let response: Response;\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (config.apiKey) {\n headers.Authorization = `Bearer ${config.apiKey}`;\n }\n try {\n response = await fetch(`${config.url}${path}`, {\n method,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(config.timeoutMs),\n headers,\n });\n } catch (err) {\n if (err instanceof Error && err.name === 'TimeoutError') {\n throw new OmlxError(\n `omlx did not respond within ${config.timeoutMs}ms at ${config.url}${path} — the model may still be loading; retry, or raise OMLX_TIMEOUT_MS`,\n { cause: err },\n );\n }\n if (err instanceof TypeError) {\n const code = networkCause(err);\n const at = code ? ` (${code})` : '';\n throw new OmlxError(\n `omlx unreachable at ${config.url}${at} — start it with: omlx serve`,\n { cause: err },\n );\n }\n throw err;\n }\n if (!response.ok) {\n if (response.status === 401) {\n throw new OmlxError(\n `omlx requires an API key for ${path} (401: ${await parseDetail(response)}) — set OMLX_API_KEY, or auth.api_key in ~/.omlx/settings.json`,\n );\n }\n throw new OmlxError(\n `omlx returned ${response.status} for ${path}: ${await parseDetail(response)}`,\n );\n }\n return (await response.json()) as T;\n}\n\nexport function omlxGet<T>(config: OmlxConfig, path: string): Promise<T> {\n return request<T>(config, path, 'GET');\n}\n\nexport function omlxPost<T>(\n config: OmlxConfig,\n path: string,\n body: unknown,\n): Promise<T> {\n return request<T>(config, path, 'POST', body);\n}\n","import { readFileSync } from 'node:fs';\n\nimport { type OmlxConfig } from '../config.js';\nimport { OmlxError } from '../errors.js';\n\nexport interface ChatCompletionInput {\n images?: readonly string[];\n max_tokens: number;\n model?: string;\n prompt: string;\n reasoning_effort?: string;\n system?: string;\n temperature?: number;\n}\n\nexport interface ChatMessage {\n content: ContentPart[] | string;\n role: 'system' | 'user';\n}\n\nexport interface ChatCompletionResponse {\n choices?: {\n message?: {\n content?: null | string;\n reasoning_content?: null | string;\n };\n }[];\n}\n\nexport interface ContentPart {\n image_url?: { url: string };\n text?: string;\n type: 'image_url' | 'text';\n}\n\nexport interface ResponseFormatJsonSchema {\n name: string;\n schema: Record<string, unknown>;\n}\n\nexport interface ResponseFormat {\n json_schema?: ResponseFormatJsonSchema;\n type: 'json_object' | 'json_schema' | 'text';\n}\n\nexport interface ChatCompletionRequest {\n max_tokens: number;\n messages: ChatMessage[];\n model: string;\n reasoning_effort?: string;\n response_format?: ResponseFormat;\n temperature?: number;\n}\n\nexport interface ChatCompletionRequest {\n max_tokens: number;\n messages: ChatMessage[];\n model: string;\n reasoning_effort?: string;\n response_format?: ResponseFormat;\n temperature?: number;\n}\n\nexport interface Completion {\n reasoningFallback: boolean;\n text: string;\n}\n\nconst MIME_SIGNATURES: readonly (readonly [\n magic: readonly number[],\n mime: string,\n])[] = [\n [[0x89, 0x50, 0x4e, 0x47], 'image/png'],\n [[0xff, 0xd8, 0xff], 'image/jpeg'],\n [[0x47, 0x49, 0x46, 0x38], 'image/gif'],\n [[0x52, 0x49, 0x46, 0x46], 'image/webp'],\n];\n\nfunction detectMime(bytes: Buffer, path: string): string {\n const mime = MIME_SIGNATURES.find(([magic]) =>\n magic.every((byte, i) => bytes[i] === byte),\n )?.[1];\n if (!mime) {\n throw new OmlxError(\n `unsupported image format at ${path} — expected png, jpeg, webp, or gif`,\n );\n }\n // RIFF is a container; only accept it when the payload is actually WEBP.\n if (\n mime === 'image/webp' &&\n bytes.subarray(8, 12).toString('ascii') !== 'WEBP'\n ) {\n throw new OmlxError(\n `unsupported image format at ${path} — expected png, jpeg, webp, or gif`,\n );\n }\n return mime;\n}\n\nexport function readImagePart(path: string): ContentPart {\n const bytes = readFileSync(path);\n const mime = detectMime(bytes, path);\n return {\n image_url: { url: `data:${mime};base64,${bytes.toString('base64')}` },\n type: 'image_url',\n };\n}\n\nexport function buildUserContent(\n prompt: string,\n images: readonly string[] | undefined,\n): ContentPart[] | string {\n if (!images || images.length === 0) {\n return prompt;\n }\n return [\n { text: prompt, type: 'text' },\n ...images.map(path => readImagePart(path)),\n ];\n}\n\nexport function buildChatCompletionRequest(\n config: OmlxConfig,\n input: ChatCompletionInput,\n responseFormat?: ResponseFormat,\n): ChatCompletionRequest {\n const messages: ChatMessage[] = [];\n if (input.system) {\n messages.push({ content: input.system, role: 'system' });\n }\n messages.push({\n content: buildUserContent(input.prompt, input.images),\n role: 'user',\n });\n return {\n max_tokens: input.max_tokens,\n messages,\n model: input.model ?? config.model,\n reasoning_effort: input.reasoning_effort,\n response_format: responseFormat,\n temperature: input.temperature,\n };\n}\n\n// The empty-answer fallback: reasoning models can spend the entire token\n// budget on reasoning_content and emit nothing for content; the reasoning\n// tail is the only signal the call produced.\nexport function extractCompletion(\n response: ChatCompletionResponse,\n): Completion {\n const message = response.choices?.[0]?.message;\n const content = message?.content?.trim();\n if (content) {\n return { reasoningFallback: false, text: content };\n }\n const reasoning = message?.reasoning_content;\n if (!reasoning) {\n throw new OmlxError(\n 'omlx returned an empty completion — no content and no reasoning_content; retry with a higher max_tokens',\n );\n }\n return { reasoningFallback: true, text: reasoning.slice(-2000) };\n}\n","import { z } from 'zod';\n\nimport type { ToolHandle } from '../server.js';\n\nimport { loadConfig, type ServerConfig } from '../config.js';\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { omlxPost } from '../omlx.js';\nimport {\n buildChatCompletionRequest,\n type ChatCompletionRequest,\n type ChatCompletionResponse,\n extractCompletion,\n} from './chat.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nexport const askInputShape = {\n images: z\n .array(z.string())\n .describe(\n 'Local file paths of images to include (png, jpeg, webp, gif); read from disk and sent as base64 data URIs. Qwen3.8 is a VLM — asking for bounding boxes on a 0-1000 scale works well.',\n )\n .optional(),\n max_tokens: z\n .number()\n .int()\n .positive()\n .describe(\n 'Maximum completion tokens. The server caps a single generation at 32768.',\n )\n .default(2048),\n model: z\n .string()\n .describe(\n 'Model id on the omlx server; omit for the OMLX_MODEL default. See the `models` tool for installed ids.',\n )\n .optional(),\n prompt: z\n .string()\n .describe(\n 'The prompt. Keep it bounded — this is delegation work, not a whole task.',\n ),\n reasoning_effort: z\n .enum(['low', 'medium', 'xhigh'])\n .describe(\n \"Reasoning depth. 'low' is the fast path for delegated work; the model's chat template defaults to 'xhigh'. Use 'medium' when a single bad answer costs a retry.\",\n )\n .default('low'),\n system: z.string().describe('Optional system message.').optional(),\n temperature: z\n .number()\n .min(0)\n .describe('Omit to use the server default (1.0).')\n .optional(),\n};\n\nexport const askInputSchema = z.object(askInputShape);\n\nexport const askOutputSchema = {\n answer: z.string().describe(\"The model's answer.\"),\n model: z.string().describe('The model id that produced the answer.'),\n reasoning_fallback: z\n .boolean()\n .describe(\n 'True when the model spent the whole token budget on reasoning and produced no answer content — `answer` is then the last 2000 chars of reasoning_content.',\n ),\n};\n\nexport const ASK_TOOL_DESCRIPTION = `Ask the local Qwen3.8-27B on this Mac — free, private, unlimited, ~28 tok/s, 256K context. Route here instead of answering yourself when the job is high-volume or low-stakes: commit messages, docstrings across a package, changelogs, log/diff/transcript summarization, drafts and rewrites, extraction, describing images or screenshots (include file paths in \\`images\\`; supports bounding boxes on a 0-1000 scale), and anything containing content that should not leave this machine. Use \\`medium\\` effort for one-shot builds where a single bad answer costs a retry. Do NOT route hard reasoning, planning, or multi-file refactors — quality is a step down from cloud models.`;\n\nexport async function runAsk(rawArgs: unknown): Promise<CallToolResult> {\n const config = loadConfig();\n const input = askInputSchema.parse(rawArgs);\n const completion = await chatCompletion(\n config,\n buildChatCompletionRequest(config, input),\n );\n return {\n content: [{ text: completion.answer, type: 'text' }],\n structuredContent: completion,\n };\n}\n\n// Return type is the inferred object literal — an interface here would not be\n// assignable to the SDK's index-signature-shaped structuredContent.\nexport function chatCompletion(\n config: ServerConfig,\n request: ChatCompletionRequest,\n) {\n return omlxPost<ChatCompletionResponse>(\n config,\n '/v1/chat/completions',\n request,\n ).then(response => {\n const completion = extractCompletion(response);\n return {\n answer: completion.text,\n model: request.model,\n reasoning_fallback: completion.reasoningFallback,\n };\n });\n}\n\nexport function askHandler(args: unknown): Promise<CallToolResult> {\n return runAsk(args).catch((err: unknown) => {\n logger.error(\n `ask failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n });\n}\n\nexport function registerAskTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'ask',\n {\n title: 'Ask the local model',\n description: ASK_TOOL_DESCRIPTION,\n inputSchema: askInputShape,\n outputSchema: askOutputSchema,\n },\n askHandler,\n );\n}\n","import { z } from 'zod';\n\nimport type { ToolHandle } from '../server.js';\n\nimport { loadConfig } from '../config.js';\nimport { OmlxError, toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { askInputShape, chatCompletion } from './ask.js';\nimport {\n buildChatCompletionRequest,\n type ChatCompletionRequest,\n} from './chat.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nexport const askStructuredInputShape = {\n ...askInputShape,\n schema: z\n .record(z.string(), z.unknown())\n .describe(\n 'JSON Schema (draft-agnostic object) the output must match — e.g. {\"type\":\"object\",\"properties\":{\"files\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"files\"]}. Top-level arrays and scalars work too.',\n ),\n schema_name: z\n .string()\n .describe('Wire metadata name for the schema.')\n .default('response'),\n};\n\nexport const askStructuredInputSchema = z.object(askStructuredInputShape);\n\nexport const askStructuredOutputSchema = {\n model: z.string().describe('The model id that produced the result.'),\n reasoning_fallback: z\n .boolean()\n .describe(\n 'True when the model produced no content — `result` is then unparsed reasoning text.',\n ),\n result: z.unknown().describe('The parsed JSON value returned by the model.'),\n};\n\nexport const ASK_STRUCTURED_TOOL_DESCRIPTION = `Ask the local Qwen3.8-27B on this Mac with schema-constrained output — the response must match the \\`schema\\` JSON Schema, returned as parsed JSON, not prose. Free, private, unlimited, ~28 tok/s, 256K context. Prefer this over asking \\`ask\\` for JSON whenever the shape is known: entities from logs, frontmatter, tables from prose, bbox JSON from a screenshot (give file paths in \\`images\\`), classification tags. Keep schemas shallow — deep nesting and long enums tax a 27B model. Do NOT route hard reasoning or multi-file analysis here — quality is a step down from cloud models.`;\n\nexport async function runAskStructured(\n rawArgs: unknown,\n): Promise<CallToolResult> {\n const config = loadConfig();\n const input = askStructuredInputSchema.parse(rawArgs);\n const request: ChatCompletionRequest = buildChatCompletionRequest(\n config,\n input,\n {\n json_schema: { name: input.schema_name, schema: input.schema },\n type: 'json_schema',\n },\n );\n\n const completion = await chatCompletion(config, request);\n let parsed: unknown;\n try {\n parsed = JSON.parse(completion.answer);\n } catch (err) {\n throw new OmlxError(\n `omlx returned non-JSON despite json_schema — first 200 chars: ${completion.answer.slice(0, 200)}; retry, or loosen the schema`,\n { cause: err },\n );\n }\n return {\n content: [{ text: JSON.stringify(parsed, null, 2), type: 'text' }],\n structuredContent: {\n model: completion.model,\n reasoning_fallback: completion.reasoning_fallback,\n result: parsed,\n },\n };\n}\n\nexport function askStructuredHandler(args: unknown): Promise<CallToolResult> {\n return runAskStructured(args).catch((err: unknown) => {\n logger.error(\n `ask_structured failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n });\n}\n\nexport function registerAskStructuredTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'ask_structured',\n {\n title: 'Ask the local model for structured output',\n description: ASK_STRUCTURED_TOOL_DESCRIPTION,\n inputSchema: askStructuredInputShape,\n outputSchema: askStructuredOutputSchema,\n },\n askStructuredHandler,\n );\n}\n","import { z } from 'zod';\n\nimport type { ToolHandle } from '../server.js';\n\nimport { loadConfig } from '../config.js';\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { omlxGet } from '../omlx.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\ninterface ModelsListResponse {\n data?: {\n id?: string;\n }[];\n}\n\ninterface ModelStatus {\n actual_size?: null | number;\n engine_type?: null | string;\n estimated_size?: null | number;\n id?: string;\n is_loading?: boolean;\n loaded?: boolean;\n max_context_window?: null | number;\n max_tokens?: null | number;\n}\n\ninterface ModelsStatusResponse {\n loaded_count?: number;\n model_count?: number;\n models?: ModelStatus[];\n}\n\nexport interface ModelSummary {\n context_window: null | number;\n engine_type: null | string;\n id: string;\n loaded: boolean;\n loading: boolean;\n max_output_tokens: null | number;\n size_bytes: null | number;\n}\n\nexport const modelsOutputSchema = {\n loaded_count: z\n .number()\n .int()\n .describe('How many models are currently in memory.'),\n model_count: z\n .number()\n .int()\n .describe('How many models are installed on disk.'),\n models: z\n .array(\n z.object({\n context_window: z\n .number()\n .int()\n .nullable()\n .describe('Maximum context window in tokens.'),\n engine_type: z\n .string()\n .nullable()\n .describe('Engine class, e.g. \"vlm\" for vision-language models.'),\n id: z\n .string()\n .describe('Model id — the `model` argument for ask/ask_structured.'),\n loaded: z\n .boolean()\n .describe('Whether the model is resident in memory.'),\n loading: z\n .boolean()\n .describe('Whether the model is loading right now.'),\n max_output_tokens: z\n .number()\n .int()\n .nullable()\n .describe('Maximum output tokens for a single generation.'),\n size_bytes: z\n .number()\n .int()\n .nullable()\n .describe('Model size on disk in bytes.'),\n }),\n )\n .describe('Installed models, sorted by id.'),\n};\n\nexport const MODELS_TOOL_DESCRIPTION = `List the models installed on the local omlx server with loaded state, context window, output cap, and size on disk — the source of truth for the \\`model\\` argument of \\`ask\\`/\\`ask_structured\\`. Read-only: load/unload stay out on purpose, the server LRU-manages the model pool itself.`;\n\nfunction summarize(status: ModelStatus): ModelSummary {\n return {\n context_window: status.max_context_window ?? null,\n engine_type: status.engine_type ?? null,\n id: status.id ?? '',\n loaded: status.loaded ?? false,\n loading: status.is_loading ?? false,\n max_output_tokens: status.max_tokens ?? null,\n size_bytes: status.actual_size ?? status.estimated_size ?? null,\n };\n}\n\nfunction formatBytes(bytes: null | number): string {\n if (bytes === null) {\n return 'size ?';\n }\n return `size ${(bytes / 1e9).toFixed(1)} GB`;\n}\n\nfunction renderModels(models: readonly ModelSummary[]): string {\n return models\n .map(\n model =>\n `${model.id} ${model.loading ? 'loading' : model.loaded ? 'loaded' : 'unloaded'} ${\n model.engine_type ?? 'engine ?'\n } context ${model.context_window ?? '?'} out ${model.max_output_tokens ?? '?'} ${formatBytes(model.size_bytes)}`,\n )\n .join('\\n');\n}\n\nexport async function runModels(): Promise<CallToolResult> {\n const config = loadConfig();\n const [list, status] = await Promise.all([\n omlxGet<ModelsListResponse>(config, '/v1/models'),\n omlxGet<ModelsStatusResponse>(config, '/v1/models/status'),\n ]);\n\n const models = (status.models ?? [])\n .map(summarize)\n .sort((a, b) => a.id.localeCompare(b.id));\n if (models.length === 0 && (list.data ?? []).length > 0) {\n // Status lagged the install; keep the ids visible at minimum.\n for (const entry of list.data ?? []) {\n if (entry.id && !models.some(model => model.id === entry.id)) {\n models.push({\n context_window: null,\n engine_type: null,\n id: entry.id,\n loaded: false,\n loading: false,\n max_output_tokens: null,\n size_bytes: null,\n });\n }\n }\n }\n\n const text = renderModels(models);\n return {\n content: [{ text, type: 'text' }],\n structuredContent: {\n loaded_count: status.loaded_count ?? 0,\n model_count: status.model_count ?? models.length,\n models,\n },\n };\n}\n\nexport function modelsHandler(): Promise<CallToolResult> {\n return runModels().catch((err: unknown) => {\n logger.error(\n `models failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n });\n}\n\nexport function registerModelsTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'models',\n {\n title: 'List installed local models',\n description: MODELS_TOOL_DESCRIPTION,\n inputSchema: {},\n outputSchema: modelsOutputSchema,\n },\n modelsHandler,\n );\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\nimport { loadConfig } from './config.js';\nimport { registerAskStructuredTool } from './tools/ask_structured.js';\nimport { registerAskTool } from './tools/ask.js';\nimport { registerModelsTool } from './tools/models.js';\n\nexport interface ToolHandle {\n remove(): void;\n}\n\nexport function createMcpServer(): McpServer {\n const { name, version, title, description, instructions } = loadConfig();\n return new McpServer({ name, version, title, description }, { instructions });\n}\n\nexport function registerTools(server: McpServer): ToolHandle[] {\n return [\n registerAskStructuredTool(server),\n registerAskTool(server),\n registerModelsTool(server),\n ];\n}\n\nexport function createServer(): McpServer {\n const server = createMcpServer();\n registerTools(server);\n return server;\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nimport { createServer } from './server.js';\n\nconst server: McpServer = createServer();\nconst transport = new StdioServerTransport();\n\nawait server.connect(transport);\n\nfunction shutdown(): void {\n void server\n .close()\n .catch(() => {\n // best-effort; exiting regardless\n })\n .finally(() => {\n // Force-exit on signal; the n/no-process-exit rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(0);\n });\n}\n\nprocess.on('SIGINT', shutdown);\nprocess.on('SIGTERM', shutdown);\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@v1nvn/omlx-mcp",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"description": "MCP server exposing a local omlx inference server (chat, structured output, model status) so coding agents can delegate bulk work to a local model.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": "dist/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"dev": "vite-node src/dev.ts",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"test:live": "RUN_LIVE=1 vitest run test/live.test.ts"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"omlx",
|
|
19
|
+
"mlx",
|
|
20
|
+
"local-llm",
|
|
21
|
+
"model-context-protocol"
|
|
22
|
+
],
|
|
23
|
+
"author": "v1nvn",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/v1nvn/agentic.git",
|
|
28
|
+
"directory": "packages/omlx-mcp"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
41
|
+
"zod": "^4.4.3"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"vite": "^8.1.4",
|
|
45
|
+
"vite-node": "^6.0.0",
|
|
46
|
+
"vitest": "^4.1.10"
|
|
47
|
+
}
|
|
48
|
+
}
|