omnikey-cli 1.0.39 → 1.0.41
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 +59 -0
- package/backend-dist/agent/agentPrompts.js +51 -6
- package/backend-dist/agent/agentServer.js +181 -33
- package/backend-dist/agent/imageTool.js +2 -1
- package/backend-dist/agent/mcpPromptCache.js +35 -0
- package/backend-dist/agent/mcpRuntime.js +245 -0
- package/backend-dist/agent/utils.js +2 -2
- package/backend-dist/index.js +7 -4
- package/backend-dist/mcpServerRoutes.js +222 -0
- package/backend-dist/models/mcpServer.js +102 -0
- package/dist/index.js +47 -7
- package/dist/mcpServer.js +334 -0
- package/package.json +2 -1
- package/src/index.ts +53 -7
- package/src/mcpServer.ts +390 -0
package/src/mcpServer.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { readConfig, getPort } from './utils';
|
|
4
|
+
|
|
5
|
+
type Transport = 'stdio' | 'http' | 'sse';
|
|
6
|
+
|
|
7
|
+
interface MCPServerDto {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string | null;
|
|
11
|
+
transport: Transport;
|
|
12
|
+
command?: string | null;
|
|
13
|
+
args: string[];
|
|
14
|
+
env: Record<string, string>;
|
|
15
|
+
url?: string | null;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
isEnabled: boolean;
|
|
18
|
+
lastConnectedAt?: string | null;
|
|
19
|
+
lastError?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function getJwt(): Promise<string> {
|
|
23
|
+
const config = readConfig();
|
|
24
|
+
const port = getPort();
|
|
25
|
+
const licenseKey = config.OMNIKEY_LICENSE_KEY || '';
|
|
26
|
+
const baseUrl = `http://localhost:${port}`;
|
|
27
|
+
const res = await axios.post(
|
|
28
|
+
`${baseUrl}/api/subscription/activate`,
|
|
29
|
+
{ licenseKey },
|
|
30
|
+
{ timeout: 10_000 },
|
|
31
|
+
);
|
|
32
|
+
return (res.data as { token: string }).token;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getBaseUrl(): string {
|
|
36
|
+
return `http://localhost:${getPort()}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function authHeaders(): Promise<Record<string, string>> {
|
|
40
|
+
let token: string;
|
|
41
|
+
try {
|
|
42
|
+
token = await getJwt();
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Authentication failed — make sure the OmniKey backend is running and your license key is configured.\nCause: ${err?.message ?? String(err)}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return { Authorization: `Bearer ${token}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseLines(input: string): string[] {
|
|
52
|
+
return input
|
|
53
|
+
.split('\n')
|
|
54
|
+
.map((l) => l.trim())
|
|
55
|
+
.filter((l) => l.length > 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseKeyValueLines(input: string): Record<string, string> {
|
|
59
|
+
const out: Record<string, string> = {};
|
|
60
|
+
for (const line of parseLines(input)) {
|
|
61
|
+
const idx = line.indexOf('=');
|
|
62
|
+
if (idx <= 0) continue;
|
|
63
|
+
const key = line.slice(0, idx).trim();
|
|
64
|
+
const value = line.slice(idx + 1).trim();
|
|
65
|
+
if (key) out[key] = value;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function promptTransportFields(
|
|
71
|
+
transport: Transport,
|
|
72
|
+
defaults?: Partial<MCPServerDto>,
|
|
73
|
+
): Promise<{
|
|
74
|
+
command?: string | null;
|
|
75
|
+
args?: string[];
|
|
76
|
+
env?: Record<string, string>;
|
|
77
|
+
url?: string | null;
|
|
78
|
+
headers?: Record<string, string>;
|
|
79
|
+
}> {
|
|
80
|
+
if (transport === 'stdio') {
|
|
81
|
+
const ans = await inquirer.prompt([
|
|
82
|
+
{
|
|
83
|
+
type: 'input',
|
|
84
|
+
name: 'command',
|
|
85
|
+
message: 'Command (executable path or name):',
|
|
86
|
+
default: defaults?.command ?? '',
|
|
87
|
+
validate: (v: string) => v.trim().length > 0 || 'Command is required for stdio transport',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'editor',
|
|
91
|
+
name: 'args',
|
|
92
|
+
message: 'Args (one per line):',
|
|
93
|
+
default: (defaults?.args ?? []).join('\n'),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'editor',
|
|
97
|
+
name: 'env',
|
|
98
|
+
message: 'Environment variables (one KEY=VALUE per line):',
|
|
99
|
+
default: formatKVForEditor(defaults?.env),
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
return {
|
|
103
|
+
command: (ans.command as string).trim(),
|
|
104
|
+
args: parseLines(ans.args as string),
|
|
105
|
+
env: parseKeyValueLines(ans.env as string),
|
|
106
|
+
url: null,
|
|
107
|
+
headers: {},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ans = await inquirer.prompt([
|
|
112
|
+
{
|
|
113
|
+
type: 'input',
|
|
114
|
+
name: 'url',
|
|
115
|
+
message: `URL for ${transport} transport:`,
|
|
116
|
+
default: defaults?.url ?? '',
|
|
117
|
+
validate: (v: string) => v.trim().length > 0 || 'URL is required',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: 'editor',
|
|
121
|
+
name: 'headers',
|
|
122
|
+
message: 'Headers (one KEY=VALUE per line):',
|
|
123
|
+
default: formatKVForEditor(defaults?.headers),
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
return {
|
|
127
|
+
url: (ans.url as string).trim(),
|
|
128
|
+
headers: parseKeyValueLines(ans.headers as string),
|
|
129
|
+
command: null,
|
|
130
|
+
args: [],
|
|
131
|
+
env: {},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatKVForEditor(dict: Record<string, string> | undefined): string {
|
|
136
|
+
if (!dict) return '';
|
|
137
|
+
return Object.entries(dict)
|
|
138
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
139
|
+
.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function mcpAdd(): Promise<void> {
|
|
143
|
+
const baseAnswers = await inquirer.prompt([
|
|
144
|
+
{
|
|
145
|
+
type: 'input',
|
|
146
|
+
name: 'name',
|
|
147
|
+
message: 'Name (unique, e.g. "github"):',
|
|
148
|
+
validate: (v: string) => v.trim().length > 0 || 'Name is required',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'input',
|
|
152
|
+
name: 'description',
|
|
153
|
+
message: 'Description (optional):',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'list',
|
|
157
|
+
name: 'transport',
|
|
158
|
+
message: 'Transport:',
|
|
159
|
+
choices: [
|
|
160
|
+
{ name: 'stdio (local process)', value: 'stdio' },
|
|
161
|
+
{ name: 'http', value: 'http' },
|
|
162
|
+
{ name: 'sse', value: 'sse' },
|
|
163
|
+
],
|
|
164
|
+
default: 'stdio',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'confirm',
|
|
168
|
+
name: 'isEnabled',
|
|
169
|
+
message: 'Enabled?',
|
|
170
|
+
default: true,
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
const transportFields = await promptTransportFields(baseAnswers.transport as Transport);
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const headers = await authHeaders();
|
|
178
|
+
const res = await axios.post<MCPServerDto>(
|
|
179
|
+
`${getBaseUrl()}/api/mcp-servers`,
|
|
180
|
+
{
|
|
181
|
+
name: (baseAnswers.name as string).trim(),
|
|
182
|
+
description: (baseAnswers.description as string).trim() || null,
|
|
183
|
+
transport: baseAnswers.transport,
|
|
184
|
+
isEnabled: baseAnswers.isEnabled,
|
|
185
|
+
...transportFields,
|
|
186
|
+
},
|
|
187
|
+
{ headers, timeout: 15_000 },
|
|
188
|
+
);
|
|
189
|
+
console.log('\nMCP server created:');
|
|
190
|
+
printServer(res.data);
|
|
191
|
+
} catch (err: any) {
|
|
192
|
+
const msg = err.response?.data?.error ?? err.message;
|
|
193
|
+
console.error(`Error creating MCP server: ${msg}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function mcpList(): Promise<void> {
|
|
198
|
+
try {
|
|
199
|
+
const headers = await authHeaders();
|
|
200
|
+
const res = await axios.get<{ servers: MCPServerDto[] }>(`${getBaseUrl()}/api/mcp-servers`, {
|
|
201
|
+
headers,
|
|
202
|
+
timeout: 10_000,
|
|
203
|
+
});
|
|
204
|
+
const { servers } = res.data;
|
|
205
|
+
if (servers.length === 0) {
|
|
206
|
+
console.log('No MCP servers installed.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
console.log('\nMCP Servers:');
|
|
210
|
+
console.log('─'.repeat(90));
|
|
211
|
+
console.log(
|
|
212
|
+
padRight('ID', 28) +
|
|
213
|
+
padRight('Name', 22) +
|
|
214
|
+
padRight('Transport', 12) +
|
|
215
|
+
padRight('Enabled', 10) +
|
|
216
|
+
'Endpoint',
|
|
217
|
+
);
|
|
218
|
+
console.log('─'.repeat(90));
|
|
219
|
+
for (const s of servers) {
|
|
220
|
+
const endpoint = s.transport === 'stdio' ? (s.command ?? '—') : (s.url ?? '—');
|
|
221
|
+
console.log(
|
|
222
|
+
padRight(s.id.slice(0, 26), 28) +
|
|
223
|
+
padRight(s.name.slice(0, 20), 22) +
|
|
224
|
+
padRight(s.transport, 12) +
|
|
225
|
+
padRight(s.isEnabled ? 'yes' : 'no', 10) +
|
|
226
|
+
endpoint.slice(0, 40),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
console.log('─'.repeat(90));
|
|
230
|
+
} catch (err: any) {
|
|
231
|
+
const msg = err.response?.data?.error ?? err.message;
|
|
232
|
+
console.error(`Error fetching MCP servers: ${msg}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function pickServer(action: string): Promise<MCPServerDto | null> {
|
|
237
|
+
const headers = await authHeaders();
|
|
238
|
+
const res = await axios.get<{ servers: MCPServerDto[] }>(`${getBaseUrl()}/api/mcp-servers`, {
|
|
239
|
+
headers,
|
|
240
|
+
timeout: 10_000,
|
|
241
|
+
});
|
|
242
|
+
const { servers } = res.data;
|
|
243
|
+
if (servers.length === 0) {
|
|
244
|
+
console.log(`No MCP servers to ${action}.`);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const { id } = await inquirer.prompt([
|
|
248
|
+
{
|
|
249
|
+
type: 'list',
|
|
250
|
+
name: 'id',
|
|
251
|
+
message: `Select an MCP server to ${action}:`,
|
|
252
|
+
choices: servers.map((s) => ({
|
|
253
|
+
name: `${s.name} [${s.transport}] ${s.isEnabled ? '✓' : '✗'}`,
|
|
254
|
+
value: s.id,
|
|
255
|
+
})),
|
|
256
|
+
},
|
|
257
|
+
]);
|
|
258
|
+
return servers.find((s) => s.id === id) ?? null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function mcpRemove(): Promise<void> {
|
|
262
|
+
try {
|
|
263
|
+
const server = await pickServer('remove');
|
|
264
|
+
if (!server) return;
|
|
265
|
+
|
|
266
|
+
const { confirm } = await inquirer.prompt([
|
|
267
|
+
{
|
|
268
|
+
type: 'confirm',
|
|
269
|
+
name: 'confirm',
|
|
270
|
+
message: `Delete MCP server "${server.name}"?`,
|
|
271
|
+
default: false,
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
274
|
+
if (!confirm) {
|
|
275
|
+
console.log('Aborted.');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const headers = await authHeaders();
|
|
280
|
+
await axios.delete(`${getBaseUrl()}/api/mcp-servers/${server.id}`, {
|
|
281
|
+
headers,
|
|
282
|
+
timeout: 10_000,
|
|
283
|
+
});
|
|
284
|
+
console.log('MCP server removed.');
|
|
285
|
+
} catch (err: any) {
|
|
286
|
+
const msg = err.response?.data?.error ?? err.message;
|
|
287
|
+
console.error(`Error removing MCP server: ${msg}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function mcpToggle(id: string): Promise<void> {
|
|
292
|
+
try {
|
|
293
|
+
const headers = await authHeaders();
|
|
294
|
+
const current = await axios.get<MCPServerDto>(`${getBaseUrl()}/api/mcp-servers/${id}`, {
|
|
295
|
+
headers,
|
|
296
|
+
timeout: 10_000,
|
|
297
|
+
});
|
|
298
|
+
const newState = !current.data.isEnabled;
|
|
299
|
+
const res = await axios.patch<MCPServerDto>(
|
|
300
|
+
`${getBaseUrl()}/api/mcp-servers/${id}`,
|
|
301
|
+
{ isEnabled: newState },
|
|
302
|
+
{ headers, timeout: 10_000 },
|
|
303
|
+
);
|
|
304
|
+
console.log(
|
|
305
|
+
`MCP server ${res.data.name} is now ${res.data.isEnabled ? 'enabled' : 'disabled'}.`,
|
|
306
|
+
);
|
|
307
|
+
} catch (err: any) {
|
|
308
|
+
const msg = err.response?.data?.error ?? err.message;
|
|
309
|
+
console.error(`Error toggling MCP server: ${msg}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function mcpUpdate(id: string): Promise<void> {
|
|
314
|
+
try {
|
|
315
|
+
const headers = await authHeaders();
|
|
316
|
+
const current = (
|
|
317
|
+
await axios.get<MCPServerDto>(`${getBaseUrl()}/api/mcp-servers/${id}`, {
|
|
318
|
+
headers,
|
|
319
|
+
timeout: 10_000,
|
|
320
|
+
})
|
|
321
|
+
).data;
|
|
322
|
+
|
|
323
|
+
const baseAnswers = await inquirer.prompt([
|
|
324
|
+
{
|
|
325
|
+
type: 'input',
|
|
326
|
+
name: 'name',
|
|
327
|
+
message: 'Name:',
|
|
328
|
+
default: current.name,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: 'input',
|
|
332
|
+
name: 'description',
|
|
333
|
+
message: 'Description:',
|
|
334
|
+
default: current.description ?? '',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
type: 'list',
|
|
338
|
+
name: 'transport',
|
|
339
|
+
message: 'Transport:',
|
|
340
|
+
choices: ['stdio', 'http', 'sse'],
|
|
341
|
+
default: current.transport,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
type: 'confirm',
|
|
345
|
+
name: 'isEnabled',
|
|
346
|
+
message: 'Enabled?',
|
|
347
|
+
default: current.isEnabled,
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
const transportFields = await promptTransportFields(
|
|
352
|
+
baseAnswers.transport as Transport,
|
|
353
|
+
current,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const res = await axios.patch<MCPServerDto>(
|
|
357
|
+
`${getBaseUrl()}/api/mcp-servers/${id}`,
|
|
358
|
+
{
|
|
359
|
+
name: (baseAnswers.name as string).trim(),
|
|
360
|
+
description: (baseAnswers.description as string).trim() || null,
|
|
361
|
+
transport: baseAnswers.transport,
|
|
362
|
+
isEnabled: baseAnswers.isEnabled,
|
|
363
|
+
...transportFields,
|
|
364
|
+
},
|
|
365
|
+
{ headers, timeout: 15_000 },
|
|
366
|
+
);
|
|
367
|
+
console.log('\nMCP server updated:');
|
|
368
|
+
printServer(res.data);
|
|
369
|
+
} catch (err: any) {
|
|
370
|
+
const msg = err.response?.data?.error ?? err.message;
|
|
371
|
+
console.error(`Error updating MCP server: ${msg}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function printServer(s: MCPServerDto): void {
|
|
376
|
+
console.log(` ID: ${s.id}`);
|
|
377
|
+
console.log(` Name: ${s.name}`);
|
|
378
|
+
console.log(` Transport: ${s.transport}`);
|
|
379
|
+
console.log(` Enabled: ${s.isEnabled}`);
|
|
380
|
+
if (s.transport === 'stdio') {
|
|
381
|
+
console.log(` Command: ${s.command ?? '—'}`);
|
|
382
|
+
if (s.args.length > 0) console.log(` Args: ${s.args.join(' ')}`);
|
|
383
|
+
} else {
|
|
384
|
+
console.log(` URL: ${s.url ?? '—'}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function padRight(str: string, width: number): string {
|
|
389
|
+
return str.length >= width ? str.slice(0, width) : str + ' '.repeat(width - str.length);
|
|
390
|
+
}
|