@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 CHANGED
@@ -64,10 +64,36 @@ yarn add @ctxprotocol/sdk
64
64
  Before using the API, complete setup at [ctxprotocol.com](https://ctxprotocol.com):
65
65
 
66
66
  1. **Sign in** — Creates your embedded wallet
67
- 2. **Enable Auto Pay** — Approve USDC spending for tool payments
67
+ 2. **Set spending cap** — Approve USDC spending on the ContextRouter (one-time setup)
68
68
  3. **Fund wallet** — Add USDC for tool execution fees
69
69
  4. **Generate API key** — In Settings page
70
70
 
71
+ ## Two Modes: Precision vs Intelligence
72
+
73
+ The SDK offers two payment models to serve different use cases:
74
+
75
+ | Mode | Method | Payment Model | Use Case |
76
+ |------|--------|---------------|----------|
77
+ | **Execute** | `client.tools.execute()` | Pay-per-request | Simple data fetches, predictable costs, building custom pipelines |
78
+ | **Query** | `client.query.run()` | Pay-per-response | Complex questions, multi-tool synthesis, curated intelligence |
79
+
80
+ **Execute mode** gives you raw data and full control — one tool, one call, one payment:
81
+ ```typescript
82
+ const result = await client.tools.execute({
83
+ toolId: "tool-uuid",
84
+ toolName: "whale_transactions",
85
+ args: { chain: "base", limit: 20 },
86
+ });
87
+ ```
88
+
89
+ **Query mode** gives you curated answers — the server handles tool discovery, multi-tool orchestration (up to 100 MCP calls per tool), self-healing retries, and AI synthesis for one flat fee:
90
+ ```typescript
91
+ const answer = await client.query.run("What are the top whale movements on Base?");
92
+ console.log(answer.response); // AI-synthesized answer
93
+ console.log(answer.toolsUsed); // Which tools were used
94
+ console.log(answer.cost); // Cost breakdown
95
+ ```
96
+
71
97
  ## Quick Start
72
98
 
73
99
  ```typescript
@@ -77,16 +103,17 @@ const client = new ContextClient({
77
103
  apiKey: "sk_live_...",
78
104
  });
79
105
 
80
- // Discover tools
81
- const tools = await client.discovery.search("gas prices");
106
+ // Pay-per-response: Ask a question, get a curated answer
107
+ const answer = await client.query.run("What are the top whale movements on Base?");
108
+ console.log(answer.response);
82
109
 
83
- // Execute a tool
110
+ // Pay-per-request: Execute a specific tool for raw data
111
+ const tools = await client.discovery.search("gas prices");
84
112
  const result = await client.tools.execute({
85
113
  toolId: tools[0].id,
86
114
  toolName: tools[0].mcpTools[0].name,
87
115
  args: { chainId: 1 },
88
116
  });
89
-
90
117
  console.log(result.result);
91
118
  ```
92
119
 
@@ -265,11 +292,11 @@ Get featured/popular tools.
265
292
  const featured = await client.discovery.getFeatured(5);
266
293
  ```
267
294
 
268
- ### Tools
295
+ ### Tools (Pay-Per-Request)
269
296
 
270
297
  #### `client.tools.execute(options)`
271
298
 
272
- Execute a tool method.
299
+ Execute a single tool method. One call, one payment, raw result.
273
300
 
274
301
  ```typescript
275
302
  const result = await client.tools.execute({
@@ -279,6 +306,48 @@ const result = await client.tools.execute({
279
306
  });
280
307
  ```
281
308
 
309
+ ### Query (Pay-Per-Response)
310
+
311
+ #### `client.query.run(options)`
312
+
313
+ Run an agentic query. The server discovers tools, executes the full pipeline (up to 100 MCP calls per tool), and returns an AI-synthesized answer.
314
+
315
+ ```typescript
316
+ // Simple string
317
+ const answer = await client.query.run("What are the top whale movements on Base?");
318
+
319
+ // With options
320
+ const answer = await client.query.run({
321
+ query: "Analyze whale activity on Base",
322
+ tools: ["tool-uuid-1", "tool-uuid-2"], // optional — auto-discover if omitted
323
+ });
324
+
325
+ console.log(answer.response); // AI-synthesized text
326
+ console.log(answer.toolsUsed); // [{ id, name, skillCalls }]
327
+ console.log(answer.cost); // { modelCostUsd, toolCostUsd, totalCostUsd }
328
+ console.log(answer.durationMs); // Total time
329
+ ```
330
+
331
+ #### `client.query.stream(options)`
332
+
333
+ Same as `run()` but streams events in real-time via SSE.
334
+
335
+ ```typescript
336
+ for await (const event of client.query.stream("What are the top whale movements?")) {
337
+ switch (event.type) {
338
+ case "tool-status":
339
+ console.log(`Tool ${event.tool.name}: ${event.status}`);
340
+ break;
341
+ case "text-delta":
342
+ process.stdout.write(event.delta);
343
+ break;
344
+ case "done":
345
+ console.log("\nTotal cost:", event.result.cost.totalCostUsd);
346
+ break;
347
+ }
348
+ }
349
+ ```
350
+
282
351
  ## Types
283
352
 
284
353
  ```typescript
@@ -296,6 +365,11 @@ import type {
296
365
  McpTool,
297
366
  ExecuteOptions,
298
367
  ExecutionResult,
368
+ // Query types (pay-per-response)
369
+ QueryOptions,
370
+ QueryResult,
371
+ QueryCost,
372
+ QueryStreamEvent,
299
373
  ContextErrorCode,
300
374
  // Auth types (for MCP server contributors)
301
375
  VerifyRequestOptions,
@@ -333,7 +407,7 @@ interface McpTool {
333
407
  }
334
408
  ```
335
409
 
336
- ### ExecutionResult
410
+ ### ExecutionResult (Pay-Per-Request)
337
411
 
338
412
  ```typescript
339
413
  interface ExecutionResult<T = unknown> {
@@ -343,6 +417,17 @@ interface ExecutionResult<T = unknown> {
343
417
  }
344
418
  ```
345
419
 
420
+ ### QueryResult (Pay-Per-Response)
421
+
422
+ ```typescript
423
+ interface QueryResult {
424
+ response: string; // AI-synthesized answer
425
+ toolsUsed: QueryToolUsage[]; // Tools used: { id, name, skillCalls }
426
+ cost: QueryCost; // { modelCostUsd, toolCostUsd, totalCostUsd }
427
+ durationMs: number;
428
+ }
429
+ ```
430
+
346
431
  ### Context Requirement Types (MCP Server Contributors)
347
432
 
348
433
  ```typescript
@@ -386,8 +471,8 @@ try {
386
471
  console.log("Setup required:", error.helpUrl);
387
472
  break;
388
473
  case "insufficient_allowance":
389
- // User needs to enable Auto Pay
390
- console.log("Enable Auto Pay:", error.helpUrl);
474
+ // User needs to set a spending cap
475
+ console.log("Set spending cap:", error.helpUrl);
391
476
  break;
392
477
  case "payment_failed":
393
478
  // Insufficient USDC balance
@@ -407,7 +492,7 @@ try {
407
492
  | ------------------------ | ---------------------------------------- | ----------------------------------- |
408
493
  | `unauthorized` | Invalid API key | Check configuration |
409
494
  | `no_wallet` | Wallet not set up | Direct user to `helpUrl` |
410
- | `insufficient_allowance` | Auto Pay not enabled | Direct user to `helpUrl` |
495
+ | `insufficient_allowance` | Spending cap not set | Direct user to `helpUrl` |
411
496
  | `payment_failed` | USDC payment failed | Check balance |
412
497
  | `execution_failed` | Tool error | Feed error to LLM for retry |
413
498
 
@@ -426,7 +511,7 @@ By adding 1 line of code to verify a JWT, Context saves you from building:
426
511
  - Refund and dispute logic
427
512
 
428
513
  **The "Stripe Webhook" Analogy:**
429
- Developers are used to verifying signatures for Stripe Webhooks or GitHub Apps. Context works the same way. When we send a request saying "Execute Tool (Payment Confirmed)", you verify the signature. Without this, anyone could curl your endpoint and drain your resources.
514
+ Developers are used to verifying signatures for Stripe Webhooks or GitHub Apps. Context works the same way. When we send a request saying "Execute Tool (Authorized)", you verify the signature. Without this, anyone could curl your endpoint and drain your resources.
430
515
 
431
516
  ### Quick Implementation (1 Line)
432
517
 
@@ -707,12 +792,13 @@ pnpm add -D @types/express
707
792
 
708
793
  ## Payment Flow
709
794
 
710
- When you execute a tool:
795
+ Context uses **deferred settlement** — tools execute first, and payment is settled in the background after the result is returned:
711
796
 
712
- 1. Your pre-approved USDC allowance is used
713
- 2. **90%** goes to the tool developer
714
- 3. **10%** goes to the protocol
715
- 4. Tool executes and returns results
797
+ 1. Your USDC spending cap (ERC-20 allowance on ContextRouter) is verified
798
+ 2. Tool executes and returns results
799
+ 3. Payment is settled server-side via the operator wallet (no client-side transactions)
800
+ 4. **90%** goes to the tool developer, **10%** goes to the protocol
801
+ 5. If execution fails, tool fees are **waived** — you only pay for successful results
716
802
 
717
803
  ## Documentation
718
804
 
@@ -1,13 +1,14 @@
1
1
  'use strict';
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,13 +428,14 @@ 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
  exports.ContextClient = ContextClient;
190
436
  exports.ContextError = ContextError;
191
437
  exports.Discovery = Discovery;
438
+ exports.Query = Query;
192
439
  exports.Tools = Tools;
193
440
  //# sourceMappingURL=index.cjs.map
194
441
  //# sourceMappingURL=index.cjs.map