@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/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { importSPKI, jwtVerify } from 'jose';
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.fetch(endpoint);
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 Auto Pay not enabled
78
- * @throws {ContextError} With code `payment_failed` if on-chain payment fails
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.fetch(
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
- 400,
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
- * All requests include the Authorization header with the API key
322
+ * Includes timeout (30s) and retry with exponential backoff for transient errors
157
323
  *
158
324
  * @internal
159
325
  */
160
- async fetch(endpoint, options = {}) {
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.json();
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 importSPKI(CONTEXT_PLATFORM_PUBLIC_KEY_PEM, "RS256");
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