@kontext-dev/js-sdk 0.1.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 +70 -0
- package/dist/adapters/ai/index.cjs +175 -0
- package/dist/adapters/ai/index.cjs.map +1 -0
- package/dist/adapters/ai/index.d.cts +51 -0
- package/dist/adapters/ai/index.d.ts +51 -0
- package/dist/adapters/ai/index.js +173 -0
- package/dist/adapters/ai/index.js.map +1 -0
- package/dist/adapters/cloudflare/index.cjs +598 -0
- package/dist/adapters/cloudflare/index.cjs.map +1 -0
- package/dist/adapters/cloudflare/index.d.cts +214 -0
- package/dist/adapters/cloudflare/index.d.ts +214 -0
- package/dist/adapters/cloudflare/index.js +594 -0
- package/dist/adapters/cloudflare/index.js.map +1 -0
- package/dist/adapters/cloudflare/react.cjs +156 -0
- package/dist/adapters/cloudflare/react.cjs.map +1 -0
- package/dist/adapters/cloudflare/react.d.cts +68 -0
- package/dist/adapters/cloudflare/react.d.ts +68 -0
- package/dist/adapters/cloudflare/react.js +152 -0
- package/dist/adapters/cloudflare/react.js.map +1 -0
- package/dist/adapters/react/index.cjs +146 -0
- package/dist/adapters/react/index.cjs.map +1 -0
- package/dist/adapters/react/index.d.cts +103 -0
- package/dist/adapters/react/index.d.ts +103 -0
- package/dist/adapters/react/index.js +142 -0
- package/dist/adapters/react/index.js.map +1 -0
- package/dist/client/index.cjs +2415 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +125 -0
- package/dist/client/index.d.ts +125 -0
- package/dist/client/index.js +2412 -0
- package/dist/client/index.js.map +1 -0
- package/dist/errors.cjs +213 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.cts +161 -0
- package/dist/errors.d.ts +161 -0
- package/dist/errors.js +201 -0
- package/dist/errors.js.map +1 -0
- package/dist/index-D5hS5PGn.d.ts +54 -0
- package/dist/index-DcL4a5Vq.d.cts +54 -0
- package/dist/index.cjs +4046 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +4029 -0
- package/dist/index.js.map +1 -0
- package/dist/kontext-CgIBANFo.d.cts +308 -0
- package/dist/kontext-CgIBANFo.d.ts +308 -0
- package/dist/management/index.cjs +867 -0
- package/dist/management/index.cjs.map +1 -0
- package/dist/management/index.d.cts +467 -0
- package/dist/management/index.d.ts +467 -0
- package/dist/management/index.js +855 -0
- package/dist/management/index.js.map +1 -0
- package/dist/mcp/index.cjs +799 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +231 -0
- package/dist/mcp/index.d.ts +231 -0
- package/dist/mcp/index.js +797 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/oauth/index.cjs +418 -0
- package/dist/oauth/index.cjs.map +1 -0
- package/dist/oauth/index.d.cts +235 -0
- package/dist/oauth/index.d.ts +235 -0
- package/dist/oauth/index.js +414 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/server/index.cjs +1634 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +10 -0
- package/dist/server/index.d.ts +10 -0
- package/dist/server/index.js +1629 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types-CzhnlJHW.d.cts +397 -0
- package/dist/types-CzhnlJHW.d.ts +397 -0
- package/dist/types-RIzHnRpk.d.cts +23 -0
- package/dist/types-RIzHnRpk.d.ts +23 -0
- package/dist/verifier-CoJmYiw3.d.cts +109 -0
- package/dist/verifier-CoJmYiw3.d.ts +109 -0
- package/dist/verify/index.cjs +319 -0
- package/dist/verify/index.cjs.map +1 -0
- package/dist/verify/index.d.cts +63 -0
- package/dist/verify/index.d.ts +63 -0
- package/dist/verify/index.js +315 -0
- package/dist/verify/index.js.map +1 -0
- package/package.json +221 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var index_js = require('@modelcontextprotocol/sdk/client/index.js');
|
|
4
|
+
var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
5
|
+
var types_js = require('@modelcontextprotocol/sdk/types.js');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
// src/mcp/client.ts
|
|
9
|
+
|
|
10
|
+
// src/storage/memory.ts
|
|
11
|
+
var MemoryStorage = class {
|
|
12
|
+
store = /* @__PURE__ */ new Map();
|
|
13
|
+
async getJson(key) {
|
|
14
|
+
const value = this.store.get(key);
|
|
15
|
+
if (value === void 0) {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
return JSON.parse(JSON.stringify(value));
|
|
19
|
+
}
|
|
20
|
+
async setJson(key, value) {
|
|
21
|
+
if (value === void 0) {
|
|
22
|
+
this.store.delete(key);
|
|
23
|
+
} else {
|
|
24
|
+
this.store.set(key, JSON.parse(JSON.stringify(value)));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Clear all stored data
|
|
29
|
+
*/
|
|
30
|
+
clear() {
|
|
31
|
+
this.store.clear();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the number of stored items
|
|
35
|
+
*/
|
|
36
|
+
get size() {
|
|
37
|
+
return this.store.size;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if a key exists
|
|
41
|
+
*/
|
|
42
|
+
has(key) {
|
|
43
|
+
return this.store.has(key);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get all keys (useful for debugging)
|
|
47
|
+
*/
|
|
48
|
+
keys() {
|
|
49
|
+
return Array.from(this.store.keys());
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/storage/types.ts
|
|
54
|
+
function createStorageKey(applicationClientId, sessionKey, ...parts) {
|
|
55
|
+
const namespace = sessionKey ? `kontext:${applicationClientId}:${sessionKey}` : `kontext:${applicationClientId}`;
|
|
56
|
+
return [namespace, ...parts].join(":");
|
|
57
|
+
}
|
|
58
|
+
var StorageKeys = {
|
|
59
|
+
// Existing keys
|
|
60
|
+
TOKENS: "tokens",
|
|
61
|
+
CODE_VERIFIER: "code_verifier",
|
|
62
|
+
STATE: "state",
|
|
63
|
+
// Pattern B (RFC 8693 Token Exchange) keys
|
|
64
|
+
/** Identity tokens (no audience) */
|
|
65
|
+
IDENTITY_TOKENS: "identity_tokens",
|
|
66
|
+
/** Prefix for resource-scoped tokens */
|
|
67
|
+
RESOURCE_TOKENS: "resource_tokens"
|
|
68
|
+
};
|
|
69
|
+
function resourceTokenKey(resource) {
|
|
70
|
+
return `${StorageKeys.RESOURCE_TOKENS}:${resource}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/errors.ts
|
|
74
|
+
var KontextError = class extends Error {
|
|
75
|
+
/** Brand field for type narrowing without instanceof */
|
|
76
|
+
kontextError = true;
|
|
77
|
+
/** Machine-readable error code, always prefixed with `kontext_` */
|
|
78
|
+
code;
|
|
79
|
+
/** HTTP status code when applicable */
|
|
80
|
+
statusCode;
|
|
81
|
+
/** Auto-generated link to error documentation */
|
|
82
|
+
docsUrl;
|
|
83
|
+
/** Server request ID for debugging / support escalation */
|
|
84
|
+
requestId;
|
|
85
|
+
/** Contextual metadata bag (integration IDs, param names, etc.) */
|
|
86
|
+
meta;
|
|
87
|
+
constructor(message, code, options) {
|
|
88
|
+
super(message, { cause: options?.cause });
|
|
89
|
+
this.name = "KontextError";
|
|
90
|
+
this.code = code;
|
|
91
|
+
this.statusCode = options?.statusCode;
|
|
92
|
+
this.requestId = options?.requestId;
|
|
93
|
+
this.meta = options?.meta ?? {};
|
|
94
|
+
this.docsUrl = `https://docs.kontext.dev/errors/${code}`;
|
|
95
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
96
|
+
}
|
|
97
|
+
toJSON() {
|
|
98
|
+
return {
|
|
99
|
+
name: this.name,
|
|
100
|
+
code: this.code,
|
|
101
|
+
message: this.message,
|
|
102
|
+
statusCode: this.statusCode,
|
|
103
|
+
docsUrl: this.docsUrl,
|
|
104
|
+
requestId: this.requestId,
|
|
105
|
+
meta: Object.keys(this.meta).length > 0 ? this.meta : void 0
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
toString() {
|
|
109
|
+
const parts = [`[${this.code}] ${this.message}`];
|
|
110
|
+
if (this.docsUrl) parts.push(`Docs: ${this.docsUrl}`);
|
|
111
|
+
if (this.requestId) parts.push(`Request ID: ${this.requestId}`);
|
|
112
|
+
return parts.join("\n");
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var AuthorizationRequiredError = class extends KontextError {
|
|
116
|
+
authorizationUrl;
|
|
117
|
+
constructor(message = "Authorization required. Complete the OAuth flow to continue.", options) {
|
|
118
|
+
super(message, "kontext_authorization_required", {
|
|
119
|
+
statusCode: 401,
|
|
120
|
+
...options
|
|
121
|
+
});
|
|
122
|
+
this.name = "AuthorizationRequiredError";
|
|
123
|
+
this.authorizationUrl = options?.authorizationUrl;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var OAuthError = class extends KontextError {
|
|
127
|
+
errorCode;
|
|
128
|
+
errorDescription;
|
|
129
|
+
constructor(message, code, options) {
|
|
130
|
+
super(message, code, {
|
|
131
|
+
statusCode: options?.statusCode ?? 400,
|
|
132
|
+
...options
|
|
133
|
+
});
|
|
134
|
+
this.name = "OAuthError";
|
|
135
|
+
this.errorCode = options?.errorCode;
|
|
136
|
+
this.errorDescription = options?.errorDescription;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
function errorProps(err) {
|
|
140
|
+
return err;
|
|
141
|
+
}
|
|
142
|
+
function isUnauthorizedError(err) {
|
|
143
|
+
const props = errorProps(err);
|
|
144
|
+
if (props.statusCode === 401 || props.status === 401) return true;
|
|
145
|
+
if (err.name === "UnauthorizedError") return true;
|
|
146
|
+
if (err.message === "Unauthorized") return true;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/oauth/provider.ts
|
|
151
|
+
var KontextOAuthProvider = class {
|
|
152
|
+
config;
|
|
153
|
+
storagePrefix;
|
|
154
|
+
pendingState = null;
|
|
155
|
+
expiryBufferMs = 60 * 1e3;
|
|
156
|
+
constructor(config) {
|
|
157
|
+
this.config = config;
|
|
158
|
+
this.storagePrefix = createStorageKey(config.clientId, config.sessionKey);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The redirect URL for OAuth callbacks
|
|
162
|
+
*/
|
|
163
|
+
get redirectUrl() {
|
|
164
|
+
return this.config.redirectUri;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* OAuth client metadata
|
|
168
|
+
*/
|
|
169
|
+
get clientMetadata() {
|
|
170
|
+
return {
|
|
171
|
+
redirect_uris: [this.config.redirectUri],
|
|
172
|
+
client_name: this.config.clientName,
|
|
173
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
174
|
+
response_types: ["code"],
|
|
175
|
+
token_endpoint_auth_method: "none"
|
|
176
|
+
// Public client
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Generate a random state parameter for OAuth CSRF protection
|
|
181
|
+
*/
|
|
182
|
+
state() {
|
|
183
|
+
const array = new Uint8Array(32);
|
|
184
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
185
|
+
globalThis.crypto.getRandomValues(array);
|
|
186
|
+
} else {
|
|
187
|
+
array.set(crypto.randomBytes(32));
|
|
188
|
+
}
|
|
189
|
+
const state = Array.from(
|
|
190
|
+
array,
|
|
191
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
192
|
+
).join("");
|
|
193
|
+
this.pendingState = state;
|
|
194
|
+
void this.config.storage.setJson(
|
|
195
|
+
this.getStorageKey(StorageKeys.STATE),
|
|
196
|
+
state
|
|
197
|
+
);
|
|
198
|
+
return state;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns the client information (client_id)
|
|
202
|
+
* Since we're a public client with pre-registered credentials,
|
|
203
|
+
* we don't use dynamic client registration.
|
|
204
|
+
*/
|
|
205
|
+
async clientInformation() {
|
|
206
|
+
return {
|
|
207
|
+
client_id: this.config.clientId
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Load stored OAuth tokens
|
|
212
|
+
*/
|
|
213
|
+
async tokens() {
|
|
214
|
+
const key = this.getStorageKey(StorageKeys.TOKENS);
|
|
215
|
+
return this.config.storage.getJson(key);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Save OAuth tokens after successful authorization
|
|
219
|
+
*/
|
|
220
|
+
async saveTokens(tokens) {
|
|
221
|
+
const key = this.getStorageKey(StorageKeys.TOKENS);
|
|
222
|
+
await this.config.storage.setJson(key, this.withIssuedAt(tokens));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Redirect the user agent to the authorization URL
|
|
226
|
+
*/
|
|
227
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
228
|
+
await this.config.onRedirectToAuthorization(authorizationUrl);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Save the PKCE code verifier before redirecting to authorization
|
|
232
|
+
*/
|
|
233
|
+
async saveCodeVerifier(codeVerifier) {
|
|
234
|
+
const key = this.getStorageKey(StorageKeys.CODE_VERIFIER);
|
|
235
|
+
await this.config.storage.setJson(key, codeVerifier);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Load the PKCE code verifier for token exchange
|
|
239
|
+
*/
|
|
240
|
+
async codeVerifier() {
|
|
241
|
+
const key = this.getStorageKey(StorageKeys.CODE_VERIFIER);
|
|
242
|
+
const verifier = await this.config.storage.getJson(key);
|
|
243
|
+
if (!verifier) {
|
|
244
|
+
throw new OAuthError(
|
|
245
|
+
"No PKCE code verifier found in storage. The OAuth flow may have expired or storage was cleared. Restart the auth flow.",
|
|
246
|
+
"kontext_oauth_code_verifier_missing"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return verifier;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Invalidate stored credentials
|
|
253
|
+
*/
|
|
254
|
+
async invalidateCredentials(scope) {
|
|
255
|
+
if (scope === "all" || scope === "tokens") {
|
|
256
|
+
const tokensKey = this.getStorageKey(StorageKeys.TOKENS);
|
|
257
|
+
await this.config.storage.setJson(tokensKey, void 0);
|
|
258
|
+
const identityKey = this.getStorageKey(StorageKeys.IDENTITY_TOKENS);
|
|
259
|
+
await this.config.storage.setJson(identityKey, void 0);
|
|
260
|
+
}
|
|
261
|
+
if (scope === "all" || scope === "verifier") {
|
|
262
|
+
const verifierKey = this.getStorageKey(StorageKeys.CODE_VERIFIER);
|
|
263
|
+
await this.config.storage.setJson(verifierKey, void 0);
|
|
264
|
+
}
|
|
265
|
+
if (scope === "all") {
|
|
266
|
+
this.pendingState = null;
|
|
267
|
+
const stateKey = this.getStorageKey(StorageKeys.STATE);
|
|
268
|
+
await this.config.storage.setJson(stateKey, void 0);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Clear all stored state (tokens, verifier, etc.)
|
|
273
|
+
* Call this to force re-authentication
|
|
274
|
+
*/
|
|
275
|
+
async clearAll() {
|
|
276
|
+
await this.invalidateCredentials("all");
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if we have valid (non-expired) tokens
|
|
280
|
+
*/
|
|
281
|
+
async hasValidTokens() {
|
|
282
|
+
const storedTokens = await this.tokens();
|
|
283
|
+
return this.isTokenValid(storedTokens);
|
|
284
|
+
}
|
|
285
|
+
// ==========================================================================
|
|
286
|
+
// Pattern B: Identity and Resource Token Management (RFC 8693)
|
|
287
|
+
// ==========================================================================
|
|
288
|
+
/**
|
|
289
|
+
* Save identity tokens (no audience)
|
|
290
|
+
* These are the tokens obtained from the initial OAuth flow before token exchange.
|
|
291
|
+
*/
|
|
292
|
+
async saveIdentityTokens(tokens) {
|
|
293
|
+
const key = this.getStorageKey(StorageKeys.IDENTITY_TOKENS);
|
|
294
|
+
await this.config.storage.setJson(key, this.withIssuedAt(tokens));
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Load identity tokens
|
|
298
|
+
* Returns the identity tokens obtained from the initial OAuth flow.
|
|
299
|
+
*/
|
|
300
|
+
async identityTokens() {
|
|
301
|
+
const key = this.getStorageKey(StorageKeys.IDENTITY_TOKENS);
|
|
302
|
+
return this.config.storage.getJson(key);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Save resource-scoped tokens for a specific resource
|
|
306
|
+
*
|
|
307
|
+
* @param resource The resource identifier (e.g., "mcp-gateway")
|
|
308
|
+
* @param tokens The resource-scoped tokens
|
|
309
|
+
*/
|
|
310
|
+
async saveResourceTokens(resource, tokens) {
|
|
311
|
+
const key = this.getStorageKey(resourceTokenKey(resource));
|
|
312
|
+
await this.config.storage.setJson(key, this.withIssuedAt(tokens));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Load resource-scoped tokens for a specific resource
|
|
316
|
+
*
|
|
317
|
+
* @param resource The resource identifier (e.g., "mcp-gateway")
|
|
318
|
+
* @returns The resource-scoped tokens, or undefined if not found
|
|
319
|
+
*/
|
|
320
|
+
async resourceTokens(resource) {
|
|
321
|
+
const key = this.getStorageKey(resourceTokenKey(resource));
|
|
322
|
+
return this.config.storage.getJson(key);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Clear resource tokens for a specific resource
|
|
326
|
+
*
|
|
327
|
+
* @param resource The resource identifier (e.g., "mcp-gateway")
|
|
328
|
+
*/
|
|
329
|
+
async clearResourceTokens(resource) {
|
|
330
|
+
const key = this.getStorageKey(resourceTokenKey(resource));
|
|
331
|
+
await this.config.storage.setJson(key, void 0);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Check if we have valid identity tokens
|
|
335
|
+
*/
|
|
336
|
+
async hasValidIdentityTokens() {
|
|
337
|
+
const tokens = await this.identityTokens();
|
|
338
|
+
return this.isTokenValid(tokens);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Check if we have valid resource tokens for a specific resource
|
|
342
|
+
*
|
|
343
|
+
* @param resource The resource identifier
|
|
344
|
+
*/
|
|
345
|
+
async hasValidResourceTokens(resource) {
|
|
346
|
+
const tokens = await this.resourceTokens(resource);
|
|
347
|
+
return this.isTokenValid(tokens);
|
|
348
|
+
}
|
|
349
|
+
async validateState(state) {
|
|
350
|
+
if (!state) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const key = this.getStorageKey(StorageKeys.STATE);
|
|
354
|
+
const storedState = this.pendingState ?? await this.config.storage.getJson(key);
|
|
355
|
+
const isValid = storedState === state;
|
|
356
|
+
if (isValid) {
|
|
357
|
+
this.pendingState = null;
|
|
358
|
+
await this.config.storage.setJson(key, void 0);
|
|
359
|
+
}
|
|
360
|
+
return isValid;
|
|
361
|
+
}
|
|
362
|
+
withIssuedAt(tokens) {
|
|
363
|
+
if (!tokens.expires_in) {
|
|
364
|
+
return tokens;
|
|
365
|
+
}
|
|
366
|
+
return { ...tokens, issued_at: Date.now() };
|
|
367
|
+
}
|
|
368
|
+
isTokenValid(tokens) {
|
|
369
|
+
if (!tokens?.access_token) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
if (!tokens.expires_in) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
const issuedAt = tokens.issued_at;
|
|
376
|
+
if (!issuedAt) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
const expiresAt = issuedAt + tokens.expires_in * 1e3;
|
|
380
|
+
return Date.now() < expiresAt - this.expiryBufferMs;
|
|
381
|
+
}
|
|
382
|
+
getStorageKey(key) {
|
|
383
|
+
return `${this.storagePrefix}:${key}`;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
function parseOAuthCallback(callbackUrl) {
|
|
387
|
+
const url = typeof callbackUrl === "string" ? new URL(callbackUrl) : callbackUrl;
|
|
388
|
+
const params = url.searchParams;
|
|
389
|
+
const error = params.get("error");
|
|
390
|
+
if (error) {
|
|
391
|
+
return {
|
|
392
|
+
error,
|
|
393
|
+
errorDescription: params.get("error_description") ?? void 0
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
code: params.get("code") ?? void 0,
|
|
398
|
+
state: params.get("state") ?? void 0
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/mcp/client.ts
|
|
403
|
+
var DEFAULT_SERVER = "https://api.kontext.dev";
|
|
404
|
+
function normalizeKontextServerUrl(server) {
|
|
405
|
+
let url = server.replace(/\/$/, "");
|
|
406
|
+
url = url.replace(/\/api\/v1\/?$/, "").replace(/\/mcp\/?$/, "");
|
|
407
|
+
url = url.replace(/\/$/, "");
|
|
408
|
+
return url;
|
|
409
|
+
}
|
|
410
|
+
var KontextMcp = class {
|
|
411
|
+
config;
|
|
412
|
+
storage;
|
|
413
|
+
oauthProvider;
|
|
414
|
+
transport = null;
|
|
415
|
+
client = null;
|
|
416
|
+
_isConnected = false;
|
|
417
|
+
_pendingConnect = null;
|
|
418
|
+
_pendingAuthFlow = null;
|
|
419
|
+
_authFlowResolve = null;
|
|
420
|
+
constructor(config) {
|
|
421
|
+
this.config = config;
|
|
422
|
+
this.storage = config.storage ?? new MemoryStorage();
|
|
423
|
+
this.oauthProvider = new KontextOAuthProvider({
|
|
424
|
+
clientId: config.clientId,
|
|
425
|
+
redirectUri: config.redirectUri,
|
|
426
|
+
storage: this.storage,
|
|
427
|
+
sessionKey: config.sessionKey ?? "default",
|
|
428
|
+
clientName: config.clientName,
|
|
429
|
+
onRedirectToAuthorization: async (url) => {
|
|
430
|
+
this._pendingAuthFlow = new Promise((resolve) => {
|
|
431
|
+
this._authFlowResolve = resolve;
|
|
432
|
+
});
|
|
433
|
+
const result = await config.onAuthRequired(url);
|
|
434
|
+
if (result) {
|
|
435
|
+
await this.handleCallback(result);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Check if we're currently connected
|
|
442
|
+
*/
|
|
443
|
+
get isConnected() {
|
|
444
|
+
return this._isConnected;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get the underlying MCP client for advanced usage
|
|
448
|
+
*/
|
|
449
|
+
get mcpClient() {
|
|
450
|
+
return this.client;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Get the session ID from the transport
|
|
454
|
+
*/
|
|
455
|
+
get sessionId() {
|
|
456
|
+
return this.transport?.sessionId;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Get the server base URL
|
|
460
|
+
*/
|
|
461
|
+
get serverUrl() {
|
|
462
|
+
return normalizeKontextServerUrl(this.config.server ?? DEFAULT_SERVER);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get the MCP endpoint URL.
|
|
466
|
+
* When `url` is provided, use it directly. Otherwise derive from server.
|
|
467
|
+
*/
|
|
468
|
+
get mcpEndpointUrl() {
|
|
469
|
+
return this.config.url ?? `${this.serverUrl}/mcp`;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* List available tools from the server
|
|
473
|
+
*
|
|
474
|
+
* This will automatically handle authentication if needed.
|
|
475
|
+
*/
|
|
476
|
+
async listTools() {
|
|
477
|
+
await this.ensureConnected();
|
|
478
|
+
const response = await this.client.listTools();
|
|
479
|
+
return response.tools;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Call a tool
|
|
483
|
+
*
|
|
484
|
+
* This will automatically handle authentication if needed.
|
|
485
|
+
*
|
|
486
|
+
* @param name The tool name
|
|
487
|
+
* @param args The tool arguments
|
|
488
|
+
*/
|
|
489
|
+
async callTool(name, args) {
|
|
490
|
+
await this.ensureConnected();
|
|
491
|
+
try {
|
|
492
|
+
const result = await this.client.callTool({
|
|
493
|
+
name,
|
|
494
|
+
arguments: args
|
|
495
|
+
});
|
|
496
|
+
return result;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (error instanceof types_js.UrlElicitationRequiredError && this.config.onElicitationUrl) {
|
|
499
|
+
for (const elicitation of error.elicitations) {
|
|
500
|
+
await this.config.onElicitationUrl({
|
|
501
|
+
url: elicitation.url,
|
|
502
|
+
message: elicitation.message ?? "Action required",
|
|
503
|
+
elicitationId: elicitation.elicitationId ?? "",
|
|
504
|
+
integrationId: elicitation.integrationId,
|
|
505
|
+
integrationName: elicitation.integrationName
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Create a connect session for the hosted connect UI.
|
|
514
|
+
*
|
|
515
|
+
* Returns a URL that can be opened in a browser to let the user
|
|
516
|
+
* connect integrations proactively (before hitting -32042 errors).
|
|
517
|
+
*/
|
|
518
|
+
async createConnectSession() {
|
|
519
|
+
const tokens = await this.oauthProvider.tokens();
|
|
520
|
+
if (!tokens?.access_token) {
|
|
521
|
+
throw new AuthorizationRequiredError(
|
|
522
|
+
"Authorization required. Complete the OAuth flow first."
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const response = await fetch(`${this.serverUrl}/mcp/connect-session`, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers: {
|
|
528
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
529
|
+
"Content-Type": "application/json"
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
if (response.status === 401) {
|
|
533
|
+
throw new AuthorizationRequiredError(
|
|
534
|
+
"Access token expired or invalid. Re-authenticate and retry."
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (!response.ok) {
|
|
538
|
+
throw new KontextError(
|
|
539
|
+
`Failed to create connect session: HTTP ${response.status}`,
|
|
540
|
+
"kontext_connect_session_failed",
|
|
541
|
+
{ statusCode: response.status }
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return await response.json();
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* List integrations attached to the current application/runtime identity.
|
|
548
|
+
*
|
|
549
|
+
* This is used by higher-level orchestrators to discover mixed integration
|
|
550
|
+
* topologies (gateway + internal credential integrations).
|
|
551
|
+
*/
|
|
552
|
+
async listRuntimeIntegrations() {
|
|
553
|
+
const tokens = await this.oauthProvider.tokens();
|
|
554
|
+
if (!tokens?.access_token) {
|
|
555
|
+
throw new AuthorizationRequiredError(
|
|
556
|
+
"Authorization required. Complete the OAuth flow first."
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const response = await fetch(`${this.serverUrl}/mcp/integrations`, {
|
|
560
|
+
method: "GET",
|
|
561
|
+
headers: {
|
|
562
|
+
Authorization: `Bearer ${tokens.access_token}`
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
if (response.status === 401) {
|
|
566
|
+
throw new AuthorizationRequiredError(
|
|
567
|
+
"Access token expired or invalid. Re-authenticate and retry."
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (!response.ok) {
|
|
571
|
+
throw new KontextError(
|
|
572
|
+
`Failed to list runtime integrations: HTTP ${response.status}`,
|
|
573
|
+
"kontext_runtime_integrations_failed",
|
|
574
|
+
{ statusCode: response.status }
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const payload = await response.json();
|
|
578
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
579
|
+
return items.map((item) => {
|
|
580
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
581
|
+
const name = typeof item.name === "string" ? item.name : id;
|
|
582
|
+
const url = typeof item.url === "string" ? item.url : "";
|
|
583
|
+
if (!id || !url) return null;
|
|
584
|
+
const category = item.category === "internal_mcp_credentials" ? "internal_mcp_credentials" : "gateway_remote_mcp";
|
|
585
|
+
const connectType = item.connectType === "credentials" || item.connectType === "oauth" || item.connectType === "none" ? item.connectType : category === "internal_mcp_credentials" ? "credentials" : item.authMode === "oauth" ? "oauth" : "none";
|
|
586
|
+
const rawConnection = item.connection && typeof item.connection === "object" ? item.connection : void 0;
|
|
587
|
+
const connected = rawConnection && typeof rawConnection.connected === "boolean" ? rawConnection.connected : false;
|
|
588
|
+
const status = rawConnection?.status === "connected" ? "connected" : "disconnected";
|
|
589
|
+
return {
|
|
590
|
+
id,
|
|
591
|
+
name,
|
|
592
|
+
url,
|
|
593
|
+
category,
|
|
594
|
+
connectType,
|
|
595
|
+
authMode: item.authMode === "oauth" || item.authMode === "user_token" || item.authMode === "server_token" || item.authMode === "none" ? item.authMode : void 0,
|
|
596
|
+
credentialSchema: item.credentialSchema,
|
|
597
|
+
requiresOauth: typeof item.requiresOauth === "boolean" ? item.requiresOauth : void 0,
|
|
598
|
+
connection: rawConnection ? {
|
|
599
|
+
connected,
|
|
600
|
+
status,
|
|
601
|
+
expiresAt: typeof rawConnection.expiresAt === "string" ? rawConnection.expiresAt : void 0,
|
|
602
|
+
displayName: typeof rawConnection.displayName === "string" ? rawConnection.displayName : void 0
|
|
603
|
+
} : void 0
|
|
604
|
+
};
|
|
605
|
+
}).filter((item) => item !== null);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Handle an OAuth callback URL
|
|
609
|
+
*
|
|
610
|
+
* Call this after the user has been redirected back from authorization.
|
|
611
|
+
* This is required for web apps where `onAuthRequired` redirects instead of waiting.
|
|
612
|
+
*
|
|
613
|
+
* @param callbackUrl The full callback URL with query parameters
|
|
614
|
+
*/
|
|
615
|
+
async handleCallback(callbackUrl) {
|
|
616
|
+
const { code, state, error, errorDescription } = parseOAuthCallback(callbackUrl);
|
|
617
|
+
if (error) {
|
|
618
|
+
this._authFlowResolve?.();
|
|
619
|
+
this._authFlowResolve = null;
|
|
620
|
+
throw new AuthorizationRequiredError(
|
|
621
|
+
errorDescription ?? `OAuth error: ${error}`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
const isValidState = await this.oauthProvider.validateState(state);
|
|
625
|
+
if (!isValidState) {
|
|
626
|
+
this._authFlowResolve?.();
|
|
627
|
+
this._authFlowResolve = null;
|
|
628
|
+
throw new OAuthError(
|
|
629
|
+
"OAuth state validation failed. The state parameter did not match. Retry the authorization flow.",
|
|
630
|
+
"kontext_oauth_state_invalid"
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (!code) {
|
|
634
|
+
this._authFlowResolve?.();
|
|
635
|
+
this._authFlowResolve = null;
|
|
636
|
+
throw new AuthorizationRequiredError(
|
|
637
|
+
"No authorization code in callback URL"
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
if (!this.transport) {
|
|
642
|
+
this.transport = new streamableHttp_js.StreamableHTTPClientTransport(
|
|
643
|
+
new URL(this.mcpEndpointUrl),
|
|
644
|
+
{
|
|
645
|
+
authProvider: this.oauthProvider
|
|
646
|
+
}
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
await this.transport.finishAuth(code);
|
|
650
|
+
const tokens = await this.oauthProvider.tokens();
|
|
651
|
+
if (!tokens?.access_token) {
|
|
652
|
+
throw new AuthorizationRequiredError("Failed to obtain tokens");
|
|
653
|
+
}
|
|
654
|
+
await this.oauthProvider.saveTokens(tokens);
|
|
655
|
+
} finally {
|
|
656
|
+
this._authFlowResolve?.();
|
|
657
|
+
this._authFlowResolve = null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Disconnect from the server
|
|
662
|
+
*/
|
|
663
|
+
async disconnect() {
|
|
664
|
+
try {
|
|
665
|
+
if (this.transport) {
|
|
666
|
+
try {
|
|
667
|
+
await this.transport.terminateSession();
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
await this.transport.close();
|
|
671
|
+
}
|
|
672
|
+
} finally {
|
|
673
|
+
this.transport = null;
|
|
674
|
+
this.client = null;
|
|
675
|
+
this._isConnected = false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Clear stored tokens and require re-authentication
|
|
680
|
+
*/
|
|
681
|
+
async clearAuth() {
|
|
682
|
+
await this.oauthProvider.clearAll();
|
|
683
|
+
await this.disconnect();
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Check if this URL is an OAuth callback
|
|
687
|
+
*
|
|
688
|
+
* Useful for web apps to detect if the current URL is a callback.
|
|
689
|
+
*
|
|
690
|
+
* @param url The URL to check
|
|
691
|
+
*/
|
|
692
|
+
isCallback(url) {
|
|
693
|
+
const urlObj = typeof url === "string" ? new URL(url) : url;
|
|
694
|
+
const redirectUri = new URL(this.config.redirectUri);
|
|
695
|
+
return urlObj.pathname === redirectUri.pathname && (urlObj.searchParams.has("code") || urlObj.searchParams.has("error"));
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Ensure we're connected, handling auth if needed
|
|
699
|
+
*/
|
|
700
|
+
async ensureConnected() {
|
|
701
|
+
if (this._isConnected && this.client) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (this._pendingConnect) {
|
|
705
|
+
await this._pendingConnect;
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this._pendingConnect = this.doConnect();
|
|
709
|
+
try {
|
|
710
|
+
await this._pendingConnect;
|
|
711
|
+
} finally {
|
|
712
|
+
this._pendingConnect = null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Internal connection logic
|
|
717
|
+
*/
|
|
718
|
+
async doConnect(authRetryCount = 0) {
|
|
719
|
+
if (!this.transport) {
|
|
720
|
+
this.transport = new streamableHttp_js.StreamableHTTPClientTransport(
|
|
721
|
+
new URL(this.mcpEndpointUrl),
|
|
722
|
+
{
|
|
723
|
+
authProvider: this.oauthProvider
|
|
724
|
+
}
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
const capabilities = {
|
|
728
|
+
tools: {}
|
|
729
|
+
};
|
|
730
|
+
if (this.config.onElicitationUrl) {
|
|
731
|
+
capabilities.elicitation = { url: {} };
|
|
732
|
+
}
|
|
733
|
+
this.client = new index_js.Client(
|
|
734
|
+
{
|
|
735
|
+
name: this.config.clientName ?? "kontext-sdk",
|
|
736
|
+
version: this.config.clientVersion ?? "0.0.1"
|
|
737
|
+
},
|
|
738
|
+
{ capabilities }
|
|
739
|
+
);
|
|
740
|
+
if (this.config.onElicitationUrl) {
|
|
741
|
+
const onElicitationUrl = this.config.onElicitationUrl;
|
|
742
|
+
this.client.setRequestHandler(types_js.ElicitRequestSchema, async (request) => {
|
|
743
|
+
const params = request.params;
|
|
744
|
+
if (params.mode === "url" && "url" in params) {
|
|
745
|
+
await onElicitationUrl({
|
|
746
|
+
url: params.url,
|
|
747
|
+
message: params.message ?? "Action required",
|
|
748
|
+
elicitationId: params.elicitationId ?? "",
|
|
749
|
+
integrationId: params.integrationId,
|
|
750
|
+
integrationName: params.integrationName
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return { action: "accept" };
|
|
754
|
+
});
|
|
755
|
+
this.client.setNotificationHandler(
|
|
756
|
+
types_js.ElicitationCompleteNotificationSchema,
|
|
757
|
+
() => {
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
await this.client.connect(this.transport);
|
|
763
|
+
this._isConnected = true;
|
|
764
|
+
} catch (error) {
|
|
765
|
+
if (error instanceof Error && isUnauthorizedError(error)) {
|
|
766
|
+
if (this._pendingAuthFlow) {
|
|
767
|
+
await this._pendingAuthFlow;
|
|
768
|
+
this._pendingAuthFlow = null;
|
|
769
|
+
this.transport = null;
|
|
770
|
+
this.client = null;
|
|
771
|
+
if (authRetryCount >= 1) {
|
|
772
|
+
throw new AuthorizationRequiredError(
|
|
773
|
+
"Authorization completed, but the MCP server still rejected the token. Verify OAuth resource audience and token issuer configuration.",
|
|
774
|
+
{ cause: error }
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
return this.doConnect(authRetryCount + 1);
|
|
778
|
+
}
|
|
779
|
+
this.transport = null;
|
|
780
|
+
this.client = null;
|
|
781
|
+
throw new AuthorizationRequiredError(
|
|
782
|
+
"Authorization required. Complete the OAuth flow and retry."
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
this.transport = null;
|
|
786
|
+
this.client = null;
|
|
787
|
+
if (error instanceof KontextError) throw error;
|
|
788
|
+
throw new KontextError(
|
|
789
|
+
`Failed to connect to MCP server at ${this.mcpEndpointUrl}. ${error instanceof Error ? error.message : String(error)}`,
|
|
790
|
+
"kontext_mcp_connection_failed",
|
|
791
|
+
{ cause: error }
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
exports.KontextMcp = KontextMcp;
|
|
798
|
+
//# sourceMappingURL=index.cjs.map
|
|
799
|
+
//# sourceMappingURL=index.cjs.map
|