ollanet 0.2.2 → 0.4.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 +112 -5
- package/dist/bench-store.d.ts +3 -0
- package/dist/bench-suite.d.ts +25 -0
- package/dist/bench.d.ts +1 -0
- package/dist/bench.js +0 -1
- package/dist/chat-store.d.ts +50 -0
- package/dist/chats.d.ts +9 -0
- package/dist/chats.js +0 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +7 -0
- package/dist/config.d.ts +45 -0
- package/dist/config.js +11 -0
- package/dist/hosts.d.ts +55 -0
- package/dist/hosts.js +9 -6
- package/dist/index.d.ts +18 -0
- package/dist/index.js +14 -0
- package/dist/mcp.d.ts +11 -0
- package/dist/mcp.js +287 -0
- package/dist/ollama-chat.d.ts +87 -0
- package/dist/paths.d.ts +17 -0
- package/dist/prompt.d.ts +41 -0
- package/dist/prompt.js +122 -66
- package/dist/scan.d.ts +44 -0
- package/dist/scan.js +56 -34
- package/package.json +27 -8
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ollanet mcp — stdio MCP server (zero deps).
|
|
3
|
+
*
|
|
4
|
+
* Speaks the Model Context Protocol over stdin/stdout so agents (Cursor,
|
|
5
|
+
* Claude Desktop, etc.) can discover hosts, prompt models, and continue chats.
|
|
6
|
+
*
|
|
7
|
+
* ollanet mcp
|
|
8
|
+
*
|
|
9
|
+
* stdout is reserved for JSON-RPC; log only to stderr.
|
|
10
|
+
*/
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { listChats, loadChat } from "./chat-store.js";
|
|
14
|
+
import { projectPath } from "./paths.js";
|
|
15
|
+
import { runPrompt } from "./prompt.js";
|
|
16
|
+
import { scanNetwork } from "./scan.js";
|
|
17
|
+
// Pin to the oldest widely-supported MCP revision. Clients may request a newer
|
|
18
|
+
// date; we still answer with this — legal negotiation, max compatibility.
|
|
19
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
20
|
+
const TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: "ollanet_scan",
|
|
23
|
+
description: "Discover reachable Ollama hosts and list their models. " +
|
|
24
|
+
"Use this before routing work to pick a host/model. " +
|
|
25
|
+
"Optional LAN TCP scan (--lan equivalent) and include offline Tailscale peers.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
lan: {
|
|
30
|
+
type: "boolean",
|
|
31
|
+
description: "TCP-scan local LAN CIDRs for open Ollama ports (default false)",
|
|
32
|
+
},
|
|
33
|
+
all: {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
description: "Also probe offline Tailscale peers (default false)",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "ollanet_prompt",
|
|
43
|
+
description: "Send a prompt to an Ollama host on the network (or continue a saved chat by id). " +
|
|
44
|
+
"Returns the assistant reply and chat_id for follow-ups. " +
|
|
45
|
+
"Machine can be a discovered name, MagicDNS name, hostname, or IP[:port].",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
prompt: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "User message to send",
|
|
52
|
+
},
|
|
53
|
+
machine: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Host name/IP (required for new chats; optional override when continuing)",
|
|
56
|
+
},
|
|
57
|
+
model: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Model name (optional; uses config default or chat model)",
|
|
60
|
+
},
|
|
61
|
+
chat_id: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Continue a saved chat by its hex hash id",
|
|
64
|
+
},
|
|
65
|
+
system: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "System prompt (new chats, or if the chat has none yet)",
|
|
68
|
+
},
|
|
69
|
+
temperature: { type: "number" },
|
|
70
|
+
num_predict: { type: "integer" },
|
|
71
|
+
num_ctx: { type: "integer" },
|
|
72
|
+
keep_alive: {
|
|
73
|
+
description: 'Keep model loaded (e.g. "5m", 0, -1)',
|
|
74
|
+
},
|
|
75
|
+
think: {
|
|
76
|
+
type: "boolean",
|
|
77
|
+
description: "Enable model thinking (default false)",
|
|
78
|
+
},
|
|
79
|
+
save: {
|
|
80
|
+
type: "boolean",
|
|
81
|
+
description: "Persist the transcript (default true)",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ["prompt"],
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "ollanet_list_chats",
|
|
90
|
+
description: "List saved ollanet chat transcripts (id, topic, machine, model, message count).",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {},
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "ollanet_get_chat",
|
|
99
|
+
description: "Load one saved chat by id, including full message history.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
chat_id: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description: "Hex chat hash from a prior prompt",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ["chat_id"],
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
function writeMessage(msg) {
|
|
114
|
+
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
115
|
+
}
|
|
116
|
+
function ok(id, result) {
|
|
117
|
+
if (id === undefined)
|
|
118
|
+
return;
|
|
119
|
+
writeMessage({ jsonrpc: "2.0", id, result });
|
|
120
|
+
}
|
|
121
|
+
function fail(id, code, message) {
|
|
122
|
+
if (id === undefined)
|
|
123
|
+
return;
|
|
124
|
+
writeMessage({ jsonrpc: "2.0", id, error: { code, message } });
|
|
125
|
+
}
|
|
126
|
+
function textResult(data, isError = false) {
|
|
127
|
+
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text }],
|
|
130
|
+
...(isError ? { isError: true } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function packageVersion() {
|
|
134
|
+
try {
|
|
135
|
+
const pkg = JSON.parse(await readFile(projectPath("package.json"), "utf8"));
|
|
136
|
+
return pkg.version ?? "0.0.0";
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return "0.0.0";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function callTool(name, args) {
|
|
143
|
+
switch (name) {
|
|
144
|
+
case "ollanet_scan": {
|
|
145
|
+
const payload = await scanNetwork({
|
|
146
|
+
lanScan: Boolean(args.lan),
|
|
147
|
+
includeOffline: Boolean(args.all),
|
|
148
|
+
});
|
|
149
|
+
return textResult(payload);
|
|
150
|
+
}
|
|
151
|
+
case "ollanet_prompt": {
|
|
152
|
+
const prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
153
|
+
if (!prompt.trim()) {
|
|
154
|
+
return textResult({ error: "prompt is required" }, true);
|
|
155
|
+
}
|
|
156
|
+
const settings = {};
|
|
157
|
+
if (typeof args.system === "string")
|
|
158
|
+
settings.system = args.system;
|
|
159
|
+
if (typeof args.temperature === "number")
|
|
160
|
+
settings.temperature = args.temperature;
|
|
161
|
+
if (typeof args.num_predict === "number")
|
|
162
|
+
settings.num_predict = Math.trunc(args.num_predict);
|
|
163
|
+
if (typeof args.num_ctx === "number")
|
|
164
|
+
settings.num_ctx = Math.trunc(args.num_ctx);
|
|
165
|
+
if (typeof args.think === "boolean")
|
|
166
|
+
settings.think = args.think;
|
|
167
|
+
if (args.keep_alive !== undefined) {
|
|
168
|
+
const raw = args.keep_alive;
|
|
169
|
+
if (typeof raw === "number")
|
|
170
|
+
settings.keep_alive = raw;
|
|
171
|
+
else if (typeof raw === "string") {
|
|
172
|
+
const asNum = Number(raw);
|
|
173
|
+
settings.keep_alive =
|
|
174
|
+
raw.trim() !== "" && Number.isFinite(asNum) && String(asNum) === raw ? asNum : raw;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const result = await runPrompt({
|
|
178
|
+
prompt,
|
|
179
|
+
machine: typeof args.machine === "string" ? args.machine : undefined,
|
|
180
|
+
model: typeof args.model === "string" ? args.model : undefined,
|
|
181
|
+
chatId: typeof args.chat_id === "string" ? args.chat_id : undefined,
|
|
182
|
+
save: args.save === undefined ? true : Boolean(args.save),
|
|
183
|
+
settings,
|
|
184
|
+
writeStdout: false,
|
|
185
|
+
stream: false,
|
|
186
|
+
quiet: true,
|
|
187
|
+
});
|
|
188
|
+
return textResult(result);
|
|
189
|
+
}
|
|
190
|
+
case "ollanet_list_chats": {
|
|
191
|
+
const chats = await listChats();
|
|
192
|
+
return textResult(chats.map((c) => ({
|
|
193
|
+
id: c.id,
|
|
194
|
+
topic: c.topic,
|
|
195
|
+
machine: c.machine,
|
|
196
|
+
model: c.model,
|
|
197
|
+
messages: c.messages.length,
|
|
198
|
+
created_at: c.created_at,
|
|
199
|
+
updated_at: c.updated_at,
|
|
200
|
+
})));
|
|
201
|
+
}
|
|
202
|
+
case "ollanet_get_chat": {
|
|
203
|
+
const id = typeof args.chat_id === "string" ? args.chat_id : "";
|
|
204
|
+
if (!id)
|
|
205
|
+
return textResult({ error: "chat_id is required" }, true);
|
|
206
|
+
const chat = await loadChat(id);
|
|
207
|
+
return textResult(chat);
|
|
208
|
+
}
|
|
209
|
+
default:
|
|
210
|
+
return textResult({ error: `Unknown tool: ${name}` }, true);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function handleRequest(msg, version) {
|
|
214
|
+
const { id, method } = msg;
|
|
215
|
+
if (!method) {
|
|
216
|
+
fail(id, -32600, "Invalid Request: missing method");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Notifications (no id) — acknowledge silently.
|
|
220
|
+
if (method === "notifications/initialized" || method.startsWith("notifications/")) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
switch (method) {
|
|
224
|
+
case "initialize":
|
|
225
|
+
ok(id, {
|
|
226
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
227
|
+
capabilities: { tools: {} },
|
|
228
|
+
serverInfo: { name: "ollanet", version },
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
case "ping":
|
|
232
|
+
ok(id, {});
|
|
233
|
+
return;
|
|
234
|
+
case "tools/list":
|
|
235
|
+
ok(id, { tools: TOOLS });
|
|
236
|
+
return;
|
|
237
|
+
case "tools/call": {
|
|
238
|
+
const params = (msg.params ?? {});
|
|
239
|
+
const name = params.name;
|
|
240
|
+
if (!name) {
|
|
241
|
+
fail(id, -32602, "tools/call requires params.name");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const result = await callTool(name, params.arguments ?? {});
|
|
246
|
+
ok(id, result);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
250
|
+
ok(id, textResult({ error: message }, true));
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
fail(id, -32601, `Method not found: ${method}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export async function main() {
|
|
259
|
+
const version = await packageVersion();
|
|
260
|
+
console.error(`ollanet mcp ${version} (stdio) — tools: ${TOOLS.map((t) => t.name).join(", ")}`);
|
|
261
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
262
|
+
for await (const line of rl) {
|
|
263
|
+
const trimmed = line.trim();
|
|
264
|
+
if (!trimmed)
|
|
265
|
+
continue;
|
|
266
|
+
let msg;
|
|
267
|
+
try {
|
|
268
|
+
msg = JSON.parse(trimmed);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
writeMessage({
|
|
272
|
+
jsonrpc: "2.0",
|
|
273
|
+
id: null,
|
|
274
|
+
error: { code: -32700, message: "Parse error" },
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// JSON-RPC batch
|
|
279
|
+
if (Array.isArray(msg)) {
|
|
280
|
+
for (const item of msg) {
|
|
281
|
+
await handleRequest(item, version);
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
await handleRequest(msg, version);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Ollama /api/chat client used by prompt and bench.
|
|
3
|
+
*/
|
|
4
|
+
import type { GenerateSettings } from "./config.js";
|
|
5
|
+
export interface ChatChunk {
|
|
6
|
+
model?: string;
|
|
7
|
+
message?: {
|
|
8
|
+
role?: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
thinking?: string;
|
|
11
|
+
};
|
|
12
|
+
done?: boolean;
|
|
13
|
+
done_reason?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
total_duration?: number;
|
|
16
|
+
load_duration?: number;
|
|
17
|
+
prompt_eval_count?: number;
|
|
18
|
+
eval_count?: number;
|
|
19
|
+
eval_duration?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface OllamaChatOptions {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
model: string;
|
|
24
|
+
messages: Array<{
|
|
25
|
+
role: string;
|
|
26
|
+
content: string;
|
|
27
|
+
}>;
|
|
28
|
+
stream: boolean;
|
|
29
|
+
settings: GenerateSettings;
|
|
30
|
+
/** Write assistant content to stdout / thinking to stderr when streaming. */
|
|
31
|
+
writeStdout: boolean;
|
|
32
|
+
/** Timeout in ms; 0 = none. */
|
|
33
|
+
timeoutMs: number;
|
|
34
|
+
}
|
|
35
|
+
export interface OllamaChatResult {
|
|
36
|
+
content: string;
|
|
37
|
+
thinking: string;
|
|
38
|
+
chunk: ChatChunk;
|
|
39
|
+
}
|
|
40
|
+
/** POST /api/chat with optional streaming and AbortSignal timeout. */
|
|
41
|
+
export declare function ollamaChat(opts: OllamaChatOptions): Promise<OllamaChatResult>;
|
|
42
|
+
export interface OllamaModelInfo {
|
|
43
|
+
name: string;
|
|
44
|
+
digest?: string;
|
|
45
|
+
size?: number;
|
|
46
|
+
details?: {
|
|
47
|
+
parameter_size?: string;
|
|
48
|
+
quantization_level?: string;
|
|
49
|
+
family?: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare function ollamaTags(baseUrl: string, timeoutMs: number): Promise<OllamaModelInfo[]>;
|
|
53
|
+
export declare function ollamaShow(baseUrl: string, model: string, timeoutMs: number): Promise<{
|
|
54
|
+
capabilities?: string[];
|
|
55
|
+
}>;
|
|
56
|
+
export declare function ollamaVersion(baseUrl: string, timeoutMs: number): Promise<string | undefined>;
|
|
57
|
+
export interface PsModel {
|
|
58
|
+
name?: string;
|
|
59
|
+
model?: string;
|
|
60
|
+
size_vram?: number;
|
|
61
|
+
context_length?: number;
|
|
62
|
+
digest?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function ollamaPs(baseUrl: string, timeoutMs: number): Promise<PsModel[]>;
|
|
65
|
+
export declare function psHasModel(models: PsModel[], name: string): boolean;
|
|
66
|
+
export declare function vramForModel(models: PsModel[], name: string): number | undefined;
|
|
67
|
+
export declare function contextLengthForModel(models: PsModel[], name: string): number | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Capability filter: omitempty / empty means "unknown → treat as completion".
|
|
70
|
+
* Only skip when a non-empty capabilities array is present and lacks completion.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isCompletionCapable(capabilities: string[] | undefined | null): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Vision-capable models (moondream, llava, …) often fail a text-only suite.
|
|
75
|
+
* Only true when capabilities are present and include vision — absent/empty → false
|
|
76
|
+
* (don't skip unknown older servers).
|
|
77
|
+
*/
|
|
78
|
+
export declare function isVisionCapable(capabilities: string[] | undefined | null): boolean;
|
|
79
|
+
/** True when capabilities are known and lack thinking (absent/empty → no warning). */
|
|
80
|
+
export declare function shouldWarnNoThinking(capabilities: string[] | undefined | null): boolean;
|
|
81
|
+
/** Poll /api/ps until model is absent (or timeout). Returns elapsed ms. */
|
|
82
|
+
export declare function waitUntilUnloaded(baseUrl: string, model: string, timeoutMs: number, pollMs?: number): Promise<{
|
|
83
|
+
unloaded: boolean;
|
|
84
|
+
waitedMs: number;
|
|
85
|
+
}>;
|
|
86
|
+
/** Request unload via keep_alive: 0 (no-op-ish if already unloaded after a short chat). */
|
|
87
|
+
export declare function ollamaUnload(baseUrl: string, model: string, timeoutMs: number): Promise<void>;
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Package root: parent of `src/` in a checkout, parent of `dist/` when installed. */
|
|
2
|
+
export declare const PROJECT_ROOT: string;
|
|
3
|
+
export declare function projectPath(...parts: string[]): string;
|
|
4
|
+
/**
|
|
5
|
+
* True when running from an installed copy (npx cache, global install, or as a
|
|
6
|
+
* dependency) rather than a source checkout. Installed copies always live under
|
|
7
|
+
* a `node_modules` directory.
|
|
8
|
+
*/
|
|
9
|
+
export declare const IS_INSTALLED: boolean;
|
|
10
|
+
/** Per-user data dir used when installed (config + chat transcripts). */
|
|
11
|
+
export declare const USER_DIR: string;
|
|
12
|
+
/** Default config location: repo-local in a checkout, `~/.ollanet/` when installed. */
|
|
13
|
+
export declare function defaultConfigPath(): string;
|
|
14
|
+
/** Default transcript dir: repo-local in a checkout, `~/.ollanet/` when installed. */
|
|
15
|
+
export declare function defaultResponsesDir(): string;
|
|
16
|
+
/** Default benchmark results dir (mirrors responses layout). */
|
|
17
|
+
export declare function defaultBenchmarksDir(): string;
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a prompt / continue a chat against an Ollama server on the network.
|
|
3
|
+
*
|
|
4
|
+
* New chat (saved by default):
|
|
5
|
+
* ollanet prompt localhost "What is Tailscale?"
|
|
6
|
+
* ollanet prompt 192.168.1.50 "Hello"
|
|
7
|
+
*
|
|
8
|
+
* Continue:
|
|
9
|
+
* ollanet prompt --chat a1b2c3d4e5f6 "Tell me more"
|
|
10
|
+
*
|
|
11
|
+
* List chats:
|
|
12
|
+
* ollanet chats
|
|
13
|
+
*/
|
|
14
|
+
import { type AppConfig, type GenerateSettings } from "./config.js";
|
|
15
|
+
export interface PromptRunOptions {
|
|
16
|
+
machine?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
prompt: string;
|
|
19
|
+
chatId?: string;
|
|
20
|
+
/** Persist transcript. Default true; pass false for one-shot app calls. */
|
|
21
|
+
save?: boolean;
|
|
22
|
+
settings?: GenerateSettings;
|
|
23
|
+
/** When false, never write the reply to stdout (MCP / programmatic). Default true for CLI. */
|
|
24
|
+
writeStdout?: boolean;
|
|
25
|
+
stream?: boolean;
|
|
26
|
+
quiet?: boolean;
|
|
27
|
+
/** In-memory config; skips the config file when set. */
|
|
28
|
+
config?: Partial<AppConfig>;
|
|
29
|
+
}
|
|
30
|
+
export interface PromptRunResult {
|
|
31
|
+
chat_id: string | null;
|
|
32
|
+
topic: string;
|
|
33
|
+
content: string;
|
|
34
|
+
thinking: string | null;
|
|
35
|
+
machine: string;
|
|
36
|
+
model: string;
|
|
37
|
+
ollama: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/** Programmatic prompt used by CLI `--json` and the MCP server. */
|
|
40
|
+
export declare function runPrompt(opts: PromptRunOptions): Promise<PromptRunResult>;
|
|
41
|
+
export declare function main(): Promise<void>;
|