@ontos-ai/knowhere-mcp 0.2.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/LICENSE +21 -0
- package/README.md +202 -0
- package/dist/chunk-ZITSXYWR.mjs +319 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +350 -0
- package/dist/index.mjs +8 -0
- package/dist/stdio.d.mts +1 -0
- package/dist/stdio.d.ts +1 -0
- package/dist/stdio.js +943 -0
- package/dist/stdio.mjs +612 -0
- package/package.json +69 -0
package/dist/stdio.mjs
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
runKnowhereMcpServer
|
|
4
|
+
} from "./chunk-ZITSXYWR.mjs";
|
|
5
|
+
|
|
6
|
+
// src/auth.ts
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
import { createServer } from "http";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { mkdir, readFile, rm, writeFile, chmod } from "fs/promises";
|
|
13
|
+
import { ValidationError } from "@ontos-ai/knowhere-sdk";
|
|
14
|
+
var DEFAULT_DASHBOARD_URL = "https://knowhereto.ai";
|
|
15
|
+
var AUTH_FILE_ENV = "KNOWHERE_MCP_AUTH_FILE";
|
|
16
|
+
var DASHBOARD_URL_ENV = "KNOWHERE_DASHBOARD_URL";
|
|
17
|
+
var API_BASE_URL_ENV = "KNOWHERE_BASE_URL";
|
|
18
|
+
var API_KEY_ENV = "KNOWHERE_API_KEY";
|
|
19
|
+
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
20
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
21
|
+
var RANDOM_BYTE_LENGTH = 32;
|
|
22
|
+
var DEFAULT_CLIENT_NAME = "knowhere-cli";
|
|
23
|
+
var DEFAULT_PERMISSION = "full_access";
|
|
24
|
+
var PERMISSION_VALUES = /* @__PURE__ */ new Set(["read_only", "full_access"]);
|
|
25
|
+
var McpCredentialManager = class {
|
|
26
|
+
authFilePath;
|
|
27
|
+
refreshPromise;
|
|
28
|
+
constructor(authFilePath = getDefaultAuthFilePath()) {
|
|
29
|
+
this.authFilePath = authFilePath;
|
|
30
|
+
}
|
|
31
|
+
getAuthFilePath() {
|
|
32
|
+
return this.authFilePath;
|
|
33
|
+
}
|
|
34
|
+
async getStatus() {
|
|
35
|
+
if (process.env[API_KEY_ENV]) {
|
|
36
|
+
return {
|
|
37
|
+
source: "api_key",
|
|
38
|
+
authFilePath: this.authFilePath,
|
|
39
|
+
apiBaseUrl: process.env[API_BASE_URL_ENV],
|
|
40
|
+
permission: DEFAULT_PERMISSION
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const storedAuth = await this.readAuth();
|
|
44
|
+
if (!storedAuth) {
|
|
45
|
+
return { source: "none", authFilePath: this.authFilePath };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
source: "stored_login",
|
|
49
|
+
authFilePath: this.authFilePath,
|
|
50
|
+
dashboardUrl: storedAuth.dashboardUrl,
|
|
51
|
+
apiBaseUrl: process.env[API_BASE_URL_ENV] ?? storedAuth.apiBaseUrl,
|
|
52
|
+
permission: storedAuth.permission,
|
|
53
|
+
refreshTokenExpiresAt: storedAuth.refreshTokenExpiresAt,
|
|
54
|
+
accessTokenExpiresAt: storedAuth.accessTokenExpiresAt
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async hasStoredLogin() {
|
|
58
|
+
return await this.readAuth() !== void 0;
|
|
59
|
+
}
|
|
60
|
+
async getAccessToken() {
|
|
61
|
+
const storedAuth = await this.readAuth();
|
|
62
|
+
if (!storedAuth) {
|
|
63
|
+
throw new ValidationError(
|
|
64
|
+
'Knowhere MCP is not logged in. Run "npx -y @ontos-ai/knowhere-mcp login" or set KNOWHERE_API_KEY.'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (isUsableAccessToken(storedAuth)) {
|
|
68
|
+
return storedAuth.accessToken;
|
|
69
|
+
}
|
|
70
|
+
if (!this.refreshPromise) {
|
|
71
|
+
this.refreshPromise = this.refreshAccessToken(storedAuth).finally(() => {
|
|
72
|
+
this.refreshPromise = void 0;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return this.refreshPromise;
|
|
76
|
+
}
|
|
77
|
+
async login(options = {}) {
|
|
78
|
+
const dashboardUrl = normalizeDashboardUrl(
|
|
79
|
+
options.dashboardUrl ?? process.env[DASHBOARD_URL_ENV] ?? DEFAULT_DASHBOARD_URL
|
|
80
|
+
);
|
|
81
|
+
const apiBaseUrl = options.baseURL ?? process.env[API_BASE_URL_ENV];
|
|
82
|
+
const codeVerifier = createRandomToken();
|
|
83
|
+
const state = createRandomToken();
|
|
84
|
+
const codeChallenge = createPkceChallenge(codeVerifier);
|
|
85
|
+
const callback = await createLoopbackCallback(state);
|
|
86
|
+
try {
|
|
87
|
+
const loginUrl = buildLoginUrl({
|
|
88
|
+
dashboardUrl,
|
|
89
|
+
redirectUri: callback.redirectUri,
|
|
90
|
+
state,
|
|
91
|
+
codeChallenge,
|
|
92
|
+
clientName: options.clientName ?? DEFAULT_CLIENT_NAME
|
|
93
|
+
});
|
|
94
|
+
options.onLoginUrl?.(loginUrl);
|
|
95
|
+
if (options.openBrowser !== false) {
|
|
96
|
+
openBrowser(loginUrl);
|
|
97
|
+
}
|
|
98
|
+
const code = await callback.waitForCode();
|
|
99
|
+
const tokenResponse = await requestToken(dashboardUrl, {
|
|
100
|
+
grant_type: "authorization_code",
|
|
101
|
+
code,
|
|
102
|
+
code_verifier: codeVerifier,
|
|
103
|
+
client_name: options.clientName ?? DEFAULT_CLIENT_NAME
|
|
104
|
+
});
|
|
105
|
+
const storedAuth = buildStoredAuth({
|
|
106
|
+
dashboardUrl,
|
|
107
|
+
apiBaseUrl,
|
|
108
|
+
tokenResponse,
|
|
109
|
+
previous: await this.readAuth()
|
|
110
|
+
});
|
|
111
|
+
await this.writeAuth(storedAuth);
|
|
112
|
+
return {
|
|
113
|
+
authFilePath: this.authFilePath,
|
|
114
|
+
dashboardUrl,
|
|
115
|
+
apiBaseUrl,
|
|
116
|
+
permission: tokenResponse.permission,
|
|
117
|
+
refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt
|
|
118
|
+
};
|
|
119
|
+
} finally {
|
|
120
|
+
await callback.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async logout() {
|
|
124
|
+
const storedAuth = await this.readAuth();
|
|
125
|
+
let revokeError;
|
|
126
|
+
if (storedAuth) {
|
|
127
|
+
try {
|
|
128
|
+
await requestRevoke(storedAuth.dashboardUrl, storedAuth.refreshToken);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
revokeError = error instanceof Error ? error.message : "Failed to revoke MCP login";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await rm(this.authFilePath, { force: true });
|
|
134
|
+
return {
|
|
135
|
+
authFilePath: this.authFilePath,
|
|
136
|
+
hadStoredLogin: storedAuth !== void 0,
|
|
137
|
+
revokeError
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async resolveBaseURL() {
|
|
141
|
+
return process.env[API_BASE_URL_ENV] ?? (await this.readAuth())?.apiBaseUrl;
|
|
142
|
+
}
|
|
143
|
+
async refreshAccessToken(storedAuth) {
|
|
144
|
+
const tokenResponse = await requestToken(storedAuth.dashboardUrl, {
|
|
145
|
+
grant_type: "refresh_token",
|
|
146
|
+
refresh_token: storedAuth.refreshToken
|
|
147
|
+
});
|
|
148
|
+
const refreshedAuth = buildStoredAuth({
|
|
149
|
+
dashboardUrl: storedAuth.dashboardUrl,
|
|
150
|
+
apiBaseUrl: storedAuth.apiBaseUrl,
|
|
151
|
+
tokenResponse,
|
|
152
|
+
previous: storedAuth
|
|
153
|
+
});
|
|
154
|
+
await this.writeAuth(refreshedAuth);
|
|
155
|
+
return refreshedAuth.accessToken ?? "";
|
|
156
|
+
}
|
|
157
|
+
async readAuth() {
|
|
158
|
+
try {
|
|
159
|
+
const rawAuth = await readFile(this.authFilePath, "utf8");
|
|
160
|
+
return parseStoredAuth(JSON.parse(rawAuth));
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (isFileMissingError(error)) {
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async writeAuth(storedAuth) {
|
|
169
|
+
await mkdir(path.dirname(this.authFilePath), { recursive: true, mode: 448 });
|
|
170
|
+
await writeFile(this.authFilePath, `${JSON.stringify(storedAuth, null, 2)}
|
|
171
|
+
`, {
|
|
172
|
+
mode: 384
|
|
173
|
+
});
|
|
174
|
+
await chmod(this.authFilePath, 384);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
function getDefaultAuthFilePath() {
|
|
178
|
+
return process.env[AUTH_FILE_ENV] ?? path.join(os.homedir(), ".knowhere-node-sdk", "mcp", "auth.json");
|
|
179
|
+
}
|
|
180
|
+
function isUsableAccessToken(storedAuth) {
|
|
181
|
+
if (!storedAuth.accessToken || !storedAuth.accessTokenExpiresAt) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const expiresAt = Date.parse(storedAuth.accessTokenExpiresAt);
|
|
185
|
+
return Number.isFinite(expiresAt) && expiresAt - Date.now() > TOKEN_REFRESH_SKEW_MS;
|
|
186
|
+
}
|
|
187
|
+
function buildStoredAuth({
|
|
188
|
+
dashboardUrl,
|
|
189
|
+
apiBaseUrl,
|
|
190
|
+
tokenResponse,
|
|
191
|
+
previous
|
|
192
|
+
}) {
|
|
193
|
+
const now = /* @__PURE__ */ new Date();
|
|
194
|
+
const accessTokenExpiresAt = new Date(
|
|
195
|
+
now.getTime() + tokenResponse.expiresInSeconds * 1e3
|
|
196
|
+
).toISOString();
|
|
197
|
+
const refreshToken = tokenResponse.refreshToken ?? previous?.refreshToken;
|
|
198
|
+
if (!refreshToken) {
|
|
199
|
+
throw new Error("Knowhere MCP token response did not include a refresh token");
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
dashboardUrl,
|
|
203
|
+
apiBaseUrl,
|
|
204
|
+
permission: tokenResponse.permission,
|
|
205
|
+
refreshToken,
|
|
206
|
+
refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt ?? previous?.refreshTokenExpiresAt,
|
|
207
|
+
accessToken: tokenResponse.accessToken,
|
|
208
|
+
accessTokenExpiresAt,
|
|
209
|
+
createdAt: previous?.createdAt ?? now.toISOString(),
|
|
210
|
+
updatedAt: now.toISOString()
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function parseStoredAuth(value) {
|
|
214
|
+
if (!isRecord(value)) {
|
|
215
|
+
throw new Error("Invalid Knowhere MCP auth file");
|
|
216
|
+
}
|
|
217
|
+
const dashboardUrl = readRequiredString(value, "dashboardUrl");
|
|
218
|
+
const refreshToken = readRequiredString(value, "refreshToken");
|
|
219
|
+
const createdAt = readRequiredString(value, "createdAt");
|
|
220
|
+
const updatedAt = readRequiredString(value, "updatedAt");
|
|
221
|
+
return {
|
|
222
|
+
dashboardUrl,
|
|
223
|
+
apiBaseUrl: readOptionalString(value, "apiBaseUrl"),
|
|
224
|
+
permission: normalizePermission(readOptionalString(value, "permission")),
|
|
225
|
+
refreshToken,
|
|
226
|
+
refreshTokenExpiresAt: readOptionalString(value, "refreshTokenExpiresAt"),
|
|
227
|
+
accessToken: readOptionalString(value, "accessToken"),
|
|
228
|
+
accessTokenExpiresAt: readOptionalString(value, "accessTokenExpiresAt"),
|
|
229
|
+
createdAt,
|
|
230
|
+
updatedAt
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function readRequiredString(value, key) {
|
|
234
|
+
const field = value[key];
|
|
235
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
236
|
+
throw new Error(`Invalid Knowhere MCP auth file: ${key} is required`);
|
|
237
|
+
}
|
|
238
|
+
return field;
|
|
239
|
+
}
|
|
240
|
+
function readOptionalString(value, key) {
|
|
241
|
+
const field = value[key];
|
|
242
|
+
return typeof field === "string" && field.length > 0 ? field : void 0;
|
|
243
|
+
}
|
|
244
|
+
function readRequiredPermission(value, key) {
|
|
245
|
+
const field = value[key];
|
|
246
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
247
|
+
throw new Error(`Invalid Knowhere MCP token response: ${key} is required`);
|
|
248
|
+
}
|
|
249
|
+
if (!PERMISSION_VALUES.has(field)) {
|
|
250
|
+
throw new Error(`Invalid Knowhere MCP token response: ${key} is invalid`);
|
|
251
|
+
}
|
|
252
|
+
return field;
|
|
253
|
+
}
|
|
254
|
+
function createPkceChallenge(codeVerifier) {
|
|
255
|
+
return crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
256
|
+
}
|
|
257
|
+
function buildLoginUrl({
|
|
258
|
+
dashboardUrl,
|
|
259
|
+
redirectUri,
|
|
260
|
+
state,
|
|
261
|
+
codeChallenge,
|
|
262
|
+
clientName
|
|
263
|
+
}) {
|
|
264
|
+
const url = new URL("/mcp/login", dashboardUrl);
|
|
265
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
266
|
+
url.searchParams.set("state", state);
|
|
267
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
268
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
269
|
+
url.searchParams.set("client_name", clientName);
|
|
270
|
+
return url.toString();
|
|
271
|
+
}
|
|
272
|
+
async function createLoopbackCallback(expectedState) {
|
|
273
|
+
let resolveCode;
|
|
274
|
+
let rejectCode;
|
|
275
|
+
let isSettled = false;
|
|
276
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
277
|
+
resolveCode = resolve;
|
|
278
|
+
rejectCode = reject;
|
|
279
|
+
});
|
|
280
|
+
const timeout = setTimeout(() => {
|
|
281
|
+
if (!isSettled) {
|
|
282
|
+
isSettled = true;
|
|
283
|
+
rejectCode?.(new Error("Timed out waiting for Dashboard login callback"));
|
|
284
|
+
}
|
|
285
|
+
}, LOGIN_TIMEOUT_MS);
|
|
286
|
+
const server = createServer((request, response) => {
|
|
287
|
+
handleCallbackRequest({
|
|
288
|
+
request,
|
|
289
|
+
response,
|
|
290
|
+
expectedState,
|
|
291
|
+
resolveCode: (code) => {
|
|
292
|
+
if (!isSettled) {
|
|
293
|
+
isSettled = true;
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
resolveCode?.(code);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
rejectCode: (error) => {
|
|
299
|
+
if (!isSettled) {
|
|
300
|
+
isSettled = true;
|
|
301
|
+
clearTimeout(timeout);
|
|
302
|
+
rejectCode?.(error);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
await new Promise((resolve, reject) => {
|
|
308
|
+
server.once("error", reject);
|
|
309
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
310
|
+
});
|
|
311
|
+
const address = server.address();
|
|
312
|
+
if (!address || typeof address === "string") {
|
|
313
|
+
throw new Error("Failed to start loopback login callback server");
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
317
|
+
waitForCode: () => codePromise,
|
|
318
|
+
close: () => new Promise((resolve, reject) => {
|
|
319
|
+
clearTimeout(timeout);
|
|
320
|
+
server.close((error) => {
|
|
321
|
+
if (error) {
|
|
322
|
+
reject(error);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
resolve();
|
|
326
|
+
});
|
|
327
|
+
})
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function handleCallbackRequest({
|
|
331
|
+
request,
|
|
332
|
+
response,
|
|
333
|
+
expectedState,
|
|
334
|
+
resolveCode,
|
|
335
|
+
rejectCode
|
|
336
|
+
}) {
|
|
337
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
338
|
+
if (requestUrl.pathname !== "/callback") {
|
|
339
|
+
response.writeHead(404);
|
|
340
|
+
response.end("Not found");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const code = requestUrl.searchParams.get("code");
|
|
344
|
+
const callbackError = requestUrl.searchParams.get("error");
|
|
345
|
+
const state = requestUrl.searchParams.get("state");
|
|
346
|
+
if (callbackError) {
|
|
347
|
+
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
348
|
+
response.end("Knowhere MCP login was denied. You can close this window.");
|
|
349
|
+
rejectCode(new Error(`Dashboard login failed: ${callbackError}`));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (!code || state !== expectedState) {
|
|
353
|
+
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
354
|
+
response.end("Knowhere MCP login failed. You can close this window.");
|
|
355
|
+
rejectCode(new Error("Dashboard login callback did not include a valid code and state"));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
359
|
+
response.end(
|
|
360
|
+
"<!doctype html><title>Knowhere MCP</title><p>Knowhere MCP login complete. You can close this window.</p>"
|
|
361
|
+
);
|
|
362
|
+
resolveCode(code);
|
|
363
|
+
}
|
|
364
|
+
async function requestToken(dashboardUrl, body) {
|
|
365
|
+
const response = await fetch(new URL("/api/mcp/token", dashboardUrl), {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: { "Content-Type": "application/json" },
|
|
368
|
+
body: JSON.stringify(body)
|
|
369
|
+
});
|
|
370
|
+
const responseBody = await readJsonResponse(response);
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
throw new Error(getResponseMessage(responseBody, "Knowhere MCP token request failed"));
|
|
373
|
+
}
|
|
374
|
+
return parseTokenResponse(responseBody);
|
|
375
|
+
}
|
|
376
|
+
async function requestRevoke(dashboardUrl, refreshToken) {
|
|
377
|
+
const response = await fetch(new URL("/api/mcp/revoke", dashboardUrl), {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: { "Content-Type": "application/json" },
|
|
380
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
381
|
+
});
|
|
382
|
+
const responseBody = await readJsonResponse(response);
|
|
383
|
+
if (!response.ok) {
|
|
384
|
+
throw new Error(getResponseMessage(responseBody, "Knowhere MCP revoke request failed"));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function readJsonResponse(response) {
|
|
388
|
+
try {
|
|
389
|
+
return await response.json();
|
|
390
|
+
} catch {
|
|
391
|
+
return void 0;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function parseTokenResponse(value) {
|
|
395
|
+
if (!isRecord(value)) {
|
|
396
|
+
throw new Error("Invalid Knowhere MCP token response");
|
|
397
|
+
}
|
|
398
|
+
const accessToken = readRequiredString(value, "accessToken");
|
|
399
|
+
const tokenType = readRequiredString(value, "tokenType");
|
|
400
|
+
const expiresInSeconds = value.expiresInSeconds;
|
|
401
|
+
const permission = readRequiredPermission(value, "permission");
|
|
402
|
+
if (tokenType !== "Bearer") {
|
|
403
|
+
throw new Error("Invalid Knowhere MCP token type");
|
|
404
|
+
}
|
|
405
|
+
if (typeof expiresInSeconds !== "number" || !Number.isFinite(expiresInSeconds)) {
|
|
406
|
+
throw new Error("Invalid Knowhere MCP token expiry");
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
accessToken,
|
|
410
|
+
expiresInSeconds,
|
|
411
|
+
permission,
|
|
412
|
+
refreshToken: readOptionalString(value, "refreshToken"),
|
|
413
|
+
refreshTokenExpiresAt: readOptionalString(value, "refreshTokenExpiresAt"),
|
|
414
|
+
tokenType
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function getResponseMessage(responseBody, fallback) {
|
|
418
|
+
if (isRecord(responseBody) && typeof responseBody.message === "string") {
|
|
419
|
+
return responseBody.message;
|
|
420
|
+
}
|
|
421
|
+
return fallback;
|
|
422
|
+
}
|
|
423
|
+
function normalizeDashboardUrl(value) {
|
|
424
|
+
const url = new URL(value);
|
|
425
|
+
url.pathname = "/";
|
|
426
|
+
url.search = "";
|
|
427
|
+
url.hash = "";
|
|
428
|
+
return url.toString();
|
|
429
|
+
}
|
|
430
|
+
function normalizePermission(value) {
|
|
431
|
+
if (value && PERMISSION_VALUES.has(value)) {
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
return DEFAULT_PERMISSION;
|
|
435
|
+
}
|
|
436
|
+
function openBrowser(url) {
|
|
437
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
438
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
439
|
+
const child = spawn(command, args, {
|
|
440
|
+
detached: true,
|
|
441
|
+
stdio: "ignore"
|
|
442
|
+
});
|
|
443
|
+
child.unref();
|
|
444
|
+
}
|
|
445
|
+
function createRandomToken() {
|
|
446
|
+
return crypto.randomBytes(RANDOM_BYTE_LENGTH).toString("base64url");
|
|
447
|
+
}
|
|
448
|
+
function isRecord(value) {
|
|
449
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
450
|
+
}
|
|
451
|
+
function isFileMissingError(error) {
|
|
452
|
+
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/stdio.ts
|
|
456
|
+
async function main() {
|
|
457
|
+
const [command, ...args] = process.argv.slice(2);
|
|
458
|
+
switch (command) {
|
|
459
|
+
case void 0:
|
|
460
|
+
case "serve":
|
|
461
|
+
await runKnowhereMcpServerWithAuth();
|
|
462
|
+
return;
|
|
463
|
+
case "login":
|
|
464
|
+
await runLogin(args);
|
|
465
|
+
return;
|
|
466
|
+
case "logout":
|
|
467
|
+
await runLogout();
|
|
468
|
+
return;
|
|
469
|
+
case "status":
|
|
470
|
+
await runStatus();
|
|
471
|
+
return;
|
|
472
|
+
case "--help":
|
|
473
|
+
case "-h":
|
|
474
|
+
case "help":
|
|
475
|
+
printHelp();
|
|
476
|
+
return;
|
|
477
|
+
default:
|
|
478
|
+
throw new Error(`Unknown knowhere-mcp command: ${command}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async function runKnowhereMcpServerWithAuth() {
|
|
482
|
+
const credentialManager = new McpCredentialManager();
|
|
483
|
+
const status = await credentialManager.getStatus();
|
|
484
|
+
if (process.env.KNOWHERE_API_KEY) {
|
|
485
|
+
await runKnowhereMcpServer({ permission: status.permission });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
await runKnowhereMcpServer({
|
|
489
|
+
authTokenProvider: () => credentialManager.getAccessToken(),
|
|
490
|
+
baseURL: await credentialManager.resolveBaseURL(),
|
|
491
|
+
permission: status.permission,
|
|
492
|
+
recoverPendingJobsOnStart: status.source === "stored_login"
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
async function runLogin(args) {
|
|
496
|
+
const options = parseLoginArgs(args);
|
|
497
|
+
const credentialManager = new McpCredentialManager(options.authFilePath);
|
|
498
|
+
const result = await credentialManager.login({
|
|
499
|
+
dashboardUrl: options.dashboardUrl,
|
|
500
|
+
baseURL: options.baseURL,
|
|
501
|
+
openBrowser: options.openBrowser,
|
|
502
|
+
clientName: options.clientName,
|
|
503
|
+
onLoginUrl: (url) => {
|
|
504
|
+
console.log(`Open this URL to log in to Knowhere:
|
|
505
|
+
${url}`);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
console.log(`Knowhere MCP login saved to ${result.authFilePath}`);
|
|
509
|
+
console.log(`Permission: ${result.permission}`);
|
|
510
|
+
if (result.refreshTokenExpiresAt) {
|
|
511
|
+
console.log(`Refresh token expires at ${result.refreshTokenExpiresAt}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function runLogout() {
|
|
515
|
+
const credentialManager = new McpCredentialManager();
|
|
516
|
+
const result = await credentialManager.logout();
|
|
517
|
+
if (result.revokeError) {
|
|
518
|
+
console.error(
|
|
519
|
+
`Knowhere MCP login was removed locally, but revoke failed: ${result.revokeError}`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
console.log(
|
|
523
|
+
result.hadStoredLogin ? `Knowhere MCP login removed from ${result.authFilePath}` : `No Knowhere MCP login found at ${result.authFilePath}`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
async function runStatus() {
|
|
527
|
+
const credentialManager = new McpCredentialManager();
|
|
528
|
+
const status = await credentialManager.getStatus();
|
|
529
|
+
console.log(formatStatus(status));
|
|
530
|
+
}
|
|
531
|
+
function parseLoginArgs(args) {
|
|
532
|
+
const options = {
|
|
533
|
+
openBrowser: true
|
|
534
|
+
};
|
|
535
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
536
|
+
const arg = args[index];
|
|
537
|
+
switch (arg) {
|
|
538
|
+
case "--dashboard-url":
|
|
539
|
+
options.dashboardUrl = readFlagValue(args, index, arg);
|
|
540
|
+
index += 1;
|
|
541
|
+
break;
|
|
542
|
+
case "--base-url":
|
|
543
|
+
options.baseURL = readFlagValue(args, index, arg);
|
|
544
|
+
index += 1;
|
|
545
|
+
break;
|
|
546
|
+
case "--auth-file":
|
|
547
|
+
options.authFilePath = readFlagValue(args, index, arg);
|
|
548
|
+
index += 1;
|
|
549
|
+
break;
|
|
550
|
+
case "--client-name":
|
|
551
|
+
options.clientName = readFlagValue(args, index, arg);
|
|
552
|
+
index += 1;
|
|
553
|
+
break;
|
|
554
|
+
case "--no-open":
|
|
555
|
+
options.openBrowser = false;
|
|
556
|
+
break;
|
|
557
|
+
default:
|
|
558
|
+
throw new Error(`Unknown login option: ${arg}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return options;
|
|
562
|
+
}
|
|
563
|
+
function readFlagValue(args, index, flag) {
|
|
564
|
+
const value = args[index + 1];
|
|
565
|
+
if (!value || value.startsWith("--")) {
|
|
566
|
+
throw new Error(`${flag} requires a value`);
|
|
567
|
+
}
|
|
568
|
+
return value;
|
|
569
|
+
}
|
|
570
|
+
function formatStatus(status) {
|
|
571
|
+
switch (status.source) {
|
|
572
|
+
case "api_key":
|
|
573
|
+
return [
|
|
574
|
+
"Knowhere MCP is authenticated with KNOWHERE_API_KEY",
|
|
575
|
+
`Auth file: ${status.authFilePath}`,
|
|
576
|
+
`Permission: ${status.permission ?? "full_access"}`
|
|
577
|
+
].join("\n");
|
|
578
|
+
case "stored_login":
|
|
579
|
+
return [
|
|
580
|
+
"Knowhere MCP is authenticated with dashboard login",
|
|
581
|
+
`Auth file: ${status.authFilePath}`,
|
|
582
|
+
`Dashboard: ${status.dashboardUrl ?? "unknown"}`,
|
|
583
|
+
`API base URL: ${status.apiBaseUrl ?? "default"}`,
|
|
584
|
+
`Permission: ${status.permission ?? "full_access"}`,
|
|
585
|
+
`Refresh token expires: ${status.refreshTokenExpiresAt ?? "unknown"}`,
|
|
586
|
+
`Access token expires: ${status.accessTokenExpiresAt ?? "not cached"}`
|
|
587
|
+
].join("\n");
|
|
588
|
+
case "none":
|
|
589
|
+
return `Knowhere MCP is not logged in
|
|
590
|
+
Run: npx -y @ontos-ai/knowhere-mcp login
|
|
591
|
+
Auth file: ${status.authFilePath}`;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function printHelp() {
|
|
595
|
+
console.log(`Usage:
|
|
596
|
+
knowhere-mcp Run the stdio MCP server
|
|
597
|
+
knowhere-mcp serve Run the stdio MCP server
|
|
598
|
+
knowhere-mcp login Log in through the Knowhere dashboard
|
|
599
|
+
knowhere-mcp status Show local MCP auth status
|
|
600
|
+
knowhere-mcp logout Remove and revoke local MCP login
|
|
601
|
+
|
|
602
|
+
Login options:
|
|
603
|
+
--dashboard-url <url> Dashboard URL, defaults to KNOWHERE_DASHBOARD_URL or https://knowhereto.ai
|
|
604
|
+
--base-url <url> Knowhere API URL, defaults to KNOWHERE_BASE_URL or SDK default
|
|
605
|
+
--auth-file <path> Override local auth file path
|
|
606
|
+
--client-name <name> Label shown in dashboard token records
|
|
607
|
+
--no-open Print login URL without opening a browser`);
|
|
608
|
+
}
|
|
609
|
+
main().catch((error) => {
|
|
610
|
+
console.error(error);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ontos-ai/knowhere-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP wrapper for the Knowhere Node.js SDK local knowledge tools",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"knowhere",
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"document-parsing"
|
|
10
|
+
],
|
|
11
|
+
"author": "Knowhere Team <team@knowhereto.ai>",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"homepage": "https://knowhereto.ai",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Ontos-AI/knowhere-node-sdk.git",
|
|
17
|
+
"directory": "packages/mcp"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/Ontos-AI/knowhere-node-sdk/issues"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"require": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"bin": {
|
|
36
|
+
"knowhere-mcp": "./dist/stdio.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20.19.0",
|
|
45
|
+
"npm": ">=10.0.0",
|
|
46
|
+
"pnpm": ">=9.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
|
+
"zod": "^3.25.76",
|
|
51
|
+
"@ontos-ai/knowhere-sdk": "0.7.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^25.2.3",
|
|
55
|
+
"tsup": "^8.0.0",
|
|
56
|
+
"typescript": "^5.3.0",
|
|
57
|
+
"vitest": "^4.0.18"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup src/index.ts src/stdio.ts --format cjs,esm --dts --clean",
|
|
61
|
+
"dev": "tsup src/index.ts src/stdio.ts --format cjs,esm --dts --watch",
|
|
62
|
+
"lint": "eslint src",
|
|
63
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
64
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
65
|
+
"test": "vitest",
|
|
66
|
+
"test:ci": "vitest run",
|
|
67
|
+
"typecheck": "tsc --noEmit"
|
|
68
|
+
}
|
|
69
|
+
}
|