@ctxprotocol/sdk 0.5.6 → 0.7.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 +103 -17
- package/dist/client/index.cjs +257 -10
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +217 -17
- package/dist/client/index.d.ts +217 -17
- package/dist/client/index.js +257 -11
- package/dist/client/index.js.map +1 -1
- package/dist/index.cjs +295 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -17
- package/dist/index.d.ts +34 -17
- package/dist/index.js +295 -13
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jwtVerify, importSPKI } from 'jose';
|
|
2
2
|
|
|
3
3
|
// src/client/types.ts
|
|
4
|
-
var ContextError = class extends Error {
|
|
4
|
+
var ContextError = class _ContextError extends Error {
|
|
5
5
|
constructor(message, code, statusCode, helpUrl) {
|
|
6
6
|
super(message);
|
|
7
7
|
this.code = code;
|
|
8
8
|
this.statusCode = statusCode;
|
|
9
9
|
this.helpUrl = helpUrl;
|
|
10
10
|
this.name = "ContextError";
|
|
11
|
+
Object.setPrototypeOf(this, _ContextError.prototype);
|
|
11
12
|
}
|
|
12
13
|
};
|
|
13
14
|
|
|
@@ -40,7 +41,7 @@ var Discovery = class {
|
|
|
40
41
|
}
|
|
41
42
|
const queryString = params.toString();
|
|
42
43
|
const endpoint = `/api/v1/tools/search${queryString ? `?${queryString}` : ""}`;
|
|
43
|
-
const response = await this.client.
|
|
44
|
+
const response = await this.client._fetch(endpoint);
|
|
44
45
|
return response.tools;
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
@@ -74,8 +75,8 @@ var Tools = class {
|
|
|
74
75
|
* @returns The execution result with the tool's output data
|
|
75
76
|
*
|
|
76
77
|
* @throws {ContextError} With code `no_wallet` if wallet not set up
|
|
77
|
-
* @throws {ContextError} With code `insufficient_allowance` if
|
|
78
|
-
* @throws {ContextError} With code `payment_failed` if
|
|
78
|
+
* @throws {ContextError} With code `insufficient_allowance` if spending cap not set
|
|
79
|
+
* @throws {ContextError} With code `payment_failed` if payment settlement fails
|
|
79
80
|
* @throws {ContextError} With code `execution_failed` if tool execution fails
|
|
80
81
|
*
|
|
81
82
|
* @example
|
|
@@ -97,7 +98,7 @@ var Tools = class {
|
|
|
97
98
|
*/
|
|
98
99
|
async execute(options) {
|
|
99
100
|
const { toolId, toolName, args } = options;
|
|
100
|
-
const response = await this.client.
|
|
101
|
+
const response = await this.client._fetch(
|
|
101
102
|
"/api/v1/tools/execute",
|
|
102
103
|
{
|
|
103
104
|
method: "POST",
|
|
@@ -108,7 +109,8 @@ var Tools = class {
|
|
|
108
109
|
throw new ContextError(
|
|
109
110
|
response.error,
|
|
110
111
|
response.code,
|
|
111
|
-
|
|
112
|
+
void 0,
|
|
113
|
+
// Don't hardcode - this was a 200 OK with error body
|
|
112
114
|
response.helpUrl
|
|
113
115
|
);
|
|
114
116
|
}
|
|
@@ -123,18 +125,174 @@ var Tools = class {
|
|
|
123
125
|
}
|
|
124
126
|
};
|
|
125
127
|
|
|
128
|
+
// src/client/resources/query.ts
|
|
129
|
+
var Query = class {
|
|
130
|
+
constructor(client) {
|
|
131
|
+
this.client = client;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Run an agentic query and wait for the full response.
|
|
135
|
+
*
|
|
136
|
+
* The server discovers relevant tools (or uses the ones you specify),
|
|
137
|
+
* executes the full agentic pipeline (up to 100 MCP calls per tool),
|
|
138
|
+
* and returns an AI-synthesized answer. Payment is settled after
|
|
139
|
+
* successful execution via deferred settlement.
|
|
140
|
+
*
|
|
141
|
+
* @param options - Query options or a plain string question
|
|
142
|
+
* @returns The complete query result with response text, tools used, and cost
|
|
143
|
+
*
|
|
144
|
+
* @throws {ContextError} With code `no_wallet` if wallet not set up
|
|
145
|
+
* @throws {ContextError} With code `insufficient_allowance` if spending cap not set
|
|
146
|
+
* @throws {ContextError} With code `payment_failed` if payment settlement fails
|
|
147
|
+
* @throws {ContextError} With code `execution_failed` if the agentic pipeline fails
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* // Simple question — server discovers tools automatically
|
|
152
|
+
* const answer = await client.query.run("What are the top whale movements on Base?");
|
|
153
|
+
* console.log(answer.response); // AI-synthesized answer
|
|
154
|
+
* console.log(answer.toolsUsed); // Which tools were used
|
|
155
|
+
* console.log(answer.cost); // Cost breakdown
|
|
156
|
+
*
|
|
157
|
+
* // With specific tools (Manual Mode)
|
|
158
|
+
* const answer = await client.query.run({
|
|
159
|
+
* query: "Analyze whale activity",
|
|
160
|
+
* tools: ["tool-uuid-1", "tool-uuid-2"],
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
async run(options) {
|
|
165
|
+
const opts = typeof options === "string" ? { query: options } : options;
|
|
166
|
+
const response = await this.client._fetch(
|
|
167
|
+
"/api/v1/query",
|
|
168
|
+
{
|
|
169
|
+
method: "POST",
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
query: opts.query,
|
|
172
|
+
tools: opts.tools,
|
|
173
|
+
stream: false
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
if ("error" in response) {
|
|
178
|
+
throw new ContextError(
|
|
179
|
+
response.error,
|
|
180
|
+
response.code,
|
|
181
|
+
void 0,
|
|
182
|
+
response.helpUrl
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (response.success) {
|
|
186
|
+
return {
|
|
187
|
+
response: response.response,
|
|
188
|
+
toolsUsed: response.toolsUsed,
|
|
189
|
+
cost: response.cost,
|
|
190
|
+
durationMs: response.durationMs
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
throw new ContextError("Unexpected response format from query API");
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Run an agentic query with streaming. Returns an async iterable that
|
|
197
|
+
* yields events as the server processes the query in real-time.
|
|
198
|
+
*
|
|
199
|
+
* Event types:
|
|
200
|
+
* - `tool-status` — A tool started executing or changed status
|
|
201
|
+
* - `text-delta` — A chunk of the AI response text
|
|
202
|
+
* - `done` — The full response is complete (includes final `QueryResult`)
|
|
203
|
+
*
|
|
204
|
+
* @param options - Query options or a plain string question
|
|
205
|
+
* @returns An async iterable of stream events
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* for await (const event of client.query.stream("What are the top whale movements?")) {
|
|
210
|
+
* switch (event.type) {
|
|
211
|
+
* case "tool-status":
|
|
212
|
+
* console.log(`Tool ${event.tool.name}: ${event.status}`);
|
|
213
|
+
* break;
|
|
214
|
+
* case "text-delta":
|
|
215
|
+
* process.stdout.write(event.delta);
|
|
216
|
+
* break;
|
|
217
|
+
* case "done":
|
|
218
|
+
* console.log("\nCost:", event.result.cost.totalCostUsd);
|
|
219
|
+
* break;
|
|
220
|
+
* }
|
|
221
|
+
* }
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
async *stream(options) {
|
|
225
|
+
const opts = typeof options === "string" ? { query: options } : options;
|
|
226
|
+
const response = await this.client._fetchRaw("/api/v1/query", {
|
|
227
|
+
method: "POST",
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
query: opts.query,
|
|
230
|
+
tools: opts.tools,
|
|
231
|
+
stream: true
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
const body = response.body;
|
|
235
|
+
if (!body) {
|
|
236
|
+
throw new ContextError("No response body for streaming query");
|
|
237
|
+
}
|
|
238
|
+
const reader = body.getReader();
|
|
239
|
+
const decoder = new TextDecoder();
|
|
240
|
+
let buffer = "";
|
|
241
|
+
try {
|
|
242
|
+
while (true) {
|
|
243
|
+
const { done, value } = await reader.read();
|
|
244
|
+
if (done) break;
|
|
245
|
+
buffer += decoder.decode(value, { stream: true });
|
|
246
|
+
const lines = buffer.split("\n");
|
|
247
|
+
buffer = lines.pop() ?? "";
|
|
248
|
+
for (const line of lines) {
|
|
249
|
+
const trimmed = line.trim();
|
|
250
|
+
if (trimmed.startsWith("data: ")) {
|
|
251
|
+
const data = trimmed.slice(6);
|
|
252
|
+
if (data === "[DONE]") return;
|
|
253
|
+
try {
|
|
254
|
+
yield JSON.parse(data);
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (buffer.trim().startsWith("data: ")) {
|
|
261
|
+
const data = buffer.trim().slice(6);
|
|
262
|
+
if (data !== "[DONE]") {
|
|
263
|
+
try {
|
|
264
|
+
yield JSON.parse(data);
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
reader.releaseLock();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
126
275
|
// src/client/client.ts
|
|
127
276
|
var ContextClient = class {
|
|
128
277
|
apiKey;
|
|
129
278
|
baseUrl;
|
|
279
|
+
_closed = false;
|
|
130
280
|
/**
|
|
131
281
|
* Discovery resource for searching tools
|
|
132
282
|
*/
|
|
133
283
|
discovery;
|
|
134
284
|
/**
|
|
135
|
-
* Tools resource for executing tools
|
|
285
|
+
* Tools resource for executing tools (pay-per-request)
|
|
136
286
|
*/
|
|
137
287
|
tools;
|
|
288
|
+
/**
|
|
289
|
+
* Query resource for agentic queries (pay-per-response).
|
|
290
|
+
*
|
|
291
|
+
* Unlike `tools.execute()` which calls a single tool once, `query` sends
|
|
292
|
+
* a natural-language question and lets the server handle tool discovery,
|
|
293
|
+
* multi-tool orchestration, self-healing, and AI synthesis — one flat fee.
|
|
294
|
+
*/
|
|
295
|
+
query;
|
|
138
296
|
/**
|
|
139
297
|
* Creates a new Context Protocol client
|
|
140
298
|
*
|
|
@@ -150,14 +308,102 @@ var ContextClient = class {
|
|
|
150
308
|
this.baseUrl = (options.baseUrl ?? "https://ctxprotocol.com").replace(/\/$/, "");
|
|
151
309
|
this.discovery = new Discovery(this);
|
|
152
310
|
this.tools = new Tools(this);
|
|
311
|
+
this.query = new Query(this);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Close the client and clean up resources.
|
|
315
|
+
* After calling close(), any in-flight requests may be aborted.
|
|
316
|
+
*/
|
|
317
|
+
close() {
|
|
318
|
+
this._closed = true;
|
|
153
319
|
}
|
|
154
320
|
/**
|
|
155
321
|
* Internal method for making authenticated HTTP requests
|
|
156
|
-
*
|
|
322
|
+
* Includes timeout (30s) and retry with exponential backoff for transient errors
|
|
157
323
|
*
|
|
158
324
|
* @internal
|
|
159
325
|
*/
|
|
160
|
-
async
|
|
326
|
+
async _fetch(endpoint, options = {}) {
|
|
327
|
+
if (this._closed) {
|
|
328
|
+
throw new ContextError("Client has been closed");
|
|
329
|
+
}
|
|
330
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
331
|
+
const maxRetries = 3;
|
|
332
|
+
const timeoutMs = 3e4;
|
|
333
|
+
let lastError;
|
|
334
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
335
|
+
const controller = new AbortController();
|
|
336
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
337
|
+
try {
|
|
338
|
+
const response = await fetch(url, {
|
|
339
|
+
...options,
|
|
340
|
+
signal: controller.signal,
|
|
341
|
+
headers: {
|
|
342
|
+
"Content-Type": "application/json",
|
|
343
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
344
|
+
...options.headers
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
clearTimeout(timeout);
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
350
|
+
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
355
|
+
let errorCode;
|
|
356
|
+
let helpUrl;
|
|
357
|
+
try {
|
|
358
|
+
const errorBody = await response.json();
|
|
359
|
+
if (errorBody.error) {
|
|
360
|
+
errorMessage = errorBody.error;
|
|
361
|
+
errorCode = errorBody.code;
|
|
362
|
+
helpUrl = errorBody.helpUrl;
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
throw new ContextError(errorMessage, errorCode, response.status, helpUrl);
|
|
367
|
+
}
|
|
368
|
+
return response.json();
|
|
369
|
+
} catch (error) {
|
|
370
|
+
clearTimeout(timeout);
|
|
371
|
+
if (error instanceof ContextError) {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
375
|
+
const isRetryable = lastError.name === "AbortError" || lastError.message.includes("fetch failed") || lastError.message.includes("ECONNRESET") || lastError.message.includes("ETIMEDOUT");
|
|
376
|
+
if (isRetryable && attempt < maxRetries) {
|
|
377
|
+
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
378
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (lastError.name === "AbortError") {
|
|
382
|
+
throw new ContextError(
|
|
383
|
+
`Request timed out after ${timeoutMs / 1e3}s`,
|
|
384
|
+
void 0,
|
|
385
|
+
408
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
throw new ContextError(
|
|
389
|
+
lastError.message,
|
|
390
|
+
void 0,
|
|
391
|
+
void 0
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
throw lastError ?? new ContextError("Request failed after retries");
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Internal method for making authenticated HTTP requests that returns
|
|
399
|
+
* the raw Response object. Used for streaming endpoints (SSE).
|
|
400
|
+
*
|
|
401
|
+
* @internal
|
|
402
|
+
*/
|
|
403
|
+
async _fetchRaw(endpoint, options = {}) {
|
|
404
|
+
if (this._closed) {
|
|
405
|
+
throw new ContextError("Client has been closed");
|
|
406
|
+
}
|
|
161
407
|
const url = `${this.baseUrl}${endpoint}`;
|
|
162
408
|
const response = await fetch(url, {
|
|
163
409
|
...options,
|
|
@@ -182,12 +428,13 @@ var ContextClient = class {
|
|
|
182
428
|
}
|
|
183
429
|
throw new ContextError(errorMessage, errorCode, response.status, helpUrl);
|
|
184
430
|
}
|
|
185
|
-
return response
|
|
431
|
+
return response;
|
|
186
432
|
}
|
|
187
433
|
};
|
|
188
434
|
|
|
189
435
|
// src/context/index.ts
|
|
190
436
|
var CONTEXT_REQUIREMENTS_KEY = "x-context-requirements";
|
|
437
|
+
var META_CONTEXT_REQUIREMENTS_KEY = "contextRequirements";
|
|
191
438
|
var CONTEXT_PLATFORM_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
192
439
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9YOgdpkmVQ5aoNovjsu
|
|
193
440
|
chJdV54OT7dUdbVXz914a7Px8EwnpDqhsvG7WO8xL8sj2Rn6ueAJBk+04Hy/P/UN
|
|
@@ -197,6 +444,41 @@ lfOKbr7w0u72dZjiZPwnNDsX6PEEgvfmoautTFYTQgnZjDzq8UimTcv3KF+hJ5Ep
|
|
|
197
444
|
weipe6amt9lzQzi8WXaFKpOXHQs//WDlUytz/Hl8pvd5craZKzo6Kyrg1Vfan7H3
|
|
198
445
|
TQIDAQAB
|
|
199
446
|
-----END PUBLIC KEY-----`;
|
|
447
|
+
var JWKS_URL = "https://ctxprotocol.com/.well-known/jwks.json";
|
|
448
|
+
var KEY_CACHE_TTL_MS = 36e5;
|
|
449
|
+
var cachedPublicKey = null;
|
|
450
|
+
var cacheTimestamp = 0;
|
|
451
|
+
async function getPlatformPublicKey() {
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
if (cachedPublicKey && now - cacheTimestamp < KEY_CACHE_TTL_MS) {
|
|
454
|
+
return cachedPublicKey;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const controller = new AbortController();
|
|
458
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
459
|
+
const response = await fetch(JWKS_URL, { signal: controller.signal });
|
|
460
|
+
clearTimeout(timeout);
|
|
461
|
+
if (response.ok) {
|
|
462
|
+
const jwks = await response.json();
|
|
463
|
+
if (jwks.keys && jwks.keys.length > 0) {
|
|
464
|
+
const key = jwks.keys[0];
|
|
465
|
+
if (key.x5c && key.x5c.length > 0) {
|
|
466
|
+
const pem = `-----BEGIN CERTIFICATE-----
|
|
467
|
+
${key.x5c[0]}
|
|
468
|
+
-----END CERTIFICATE-----`;
|
|
469
|
+
const { importX509 } = await import('jose');
|
|
470
|
+
cachedPublicKey = await importX509(pem, "RS256");
|
|
471
|
+
cacheTimestamp = now;
|
|
472
|
+
return cachedPublicKey;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
cachedPublicKey = await importSPKI(CONTEXT_PLATFORM_PUBLIC_KEY_PEM, "RS256");
|
|
479
|
+
cacheTimestamp = now;
|
|
480
|
+
return cachedPublicKey;
|
|
481
|
+
}
|
|
200
482
|
var PROTECTED_MCP_METHODS = /* @__PURE__ */ new Set([
|
|
201
483
|
"tools/call"
|
|
202
484
|
// Uncomment these if you want to protect resource/prompt access:
|
|
@@ -228,7 +510,7 @@ async function verifyContextRequest(options) {
|
|
|
228
510
|
}
|
|
229
511
|
const token = authorizationHeader.split(" ")[1];
|
|
230
512
|
try {
|
|
231
|
-
const publicKey = await
|
|
513
|
+
const publicKey = await getPlatformPublicKey();
|
|
232
514
|
const { payload } = await jwtVerify(token, publicKey, {
|
|
233
515
|
issuer: "https://ctxprotocol.com",
|
|
234
516
|
audience
|
|
@@ -312,6 +594,6 @@ function wrapHandshakeResponse(action) {
|
|
|
312
594
|
};
|
|
313
595
|
}
|
|
314
596
|
|
|
315
|
-
export { CONTEXT_REQUIREMENTS_KEY, ContextClient, ContextError, Discovery, Tools, createAuthRequired, createContextMiddleware, createSignatureRequest, createTransactionProposal, isAuthRequired, isHandshakeAction, isOpenMcpMethod, isProtectedMcpMethod, isSignatureRequest, isTransactionProposal, verifyContextRequest, wrapHandshakeResponse };
|
|
597
|
+
export { CONTEXT_REQUIREMENTS_KEY, ContextClient, ContextError, Discovery, META_CONTEXT_REQUIREMENTS_KEY, Query, Tools, createAuthRequired, createContextMiddleware, createSignatureRequest, createTransactionProposal, isAuthRequired, isHandshakeAction, isOpenMcpMethod, isProtectedMcpMethod, isSignatureRequest, isTransactionProposal, verifyContextRequest, wrapHandshakeResponse };
|
|
316
598
|
//# sourceMappingURL=index.js.map
|
|
317
599
|
//# sourceMappingURL=index.js.map
|