@ctxprotocol/sdk 0.8.1 → 0.8.3

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
@@ -28,13 +28,13 @@ We're funding the initial supply of MCP Tools for the Context Marketplace. **Bec
28
28
  - **🔌 One Interface, Everything:** Stop integrating APIs one by one. Use a single SDK to access any tool in the marketplace.
29
29
  - **🧠 Zero-Ops:** We're a gateway to the best MCP tools. Just send the JSON and get the result.
30
30
  - **⚡️ Agentic Discovery:** Your Agent can search the marketplace at runtime to find tools it didn't know it needed.
31
- - **💸 Pay-Per-Response:** The $500/year subscription? Now $0.01/response. No monthly fees, just results.
31
+ - **💸 Dual-Surface Economics:** Use Query for pay-per-response intelligence or Execute for session-budgeted method calls.
32
32
 
33
33
  ## Who Is This SDK For?
34
34
 
35
35
  | Role | What You Use |
36
36
  |------|--------------|
37
- | **AI Agent Developer** | `@ctxprotocol/sdk` — Query marketplace, execute tools, handle payments |
37
+ | **AI Agent Developer** | `@ctxprotocol/sdk` — Query curated answers or Execute with explicit method pricing + sessions |
38
38
  | **Tool Contributor (Data Broker)** | `@modelcontextprotocol/sdk` + `@ctxprotocol/sdk` — Standard MCP server + security middleware |
39
39
 
40
40
  **For AI Agent Developers:** Use this SDK to search the marketplace, execute tools, and handle micro-payments.
@@ -72,25 +72,36 @@ Before using the API, complete setup at [ctxprotocol.com](https://ctxprotocol.co
72
72
 
73
73
  The SDK offers two payment models to serve different use cases:
74
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 |
75
+ | Mode | Method | Payment Model | Settlement Shape | Use Case |
76
+ |------|--------|---------------|------------------|----------|
77
+ | **Execute** | `client.tools.execute()` | Per execute call | Session accrual + deferred batch flush | Deterministic pipelines, raw outputs, explicit spend envelopes |
78
+ | **Query** | `client.query.run()` | Pay-per-response | Deferred post-response | Complex questions, multi-tool synthesis, curated intelligence |
79
79
 
80
- **Execute mode** gives you raw data and full control one tool, one call, one payment:
80
+ **Execute mode** gives you raw data and full control with explicit method pricing and session budgets:
81
81
  ```typescript
82
+ const session = await client.tools.startSession({ maxSpendUsd: "2.00" });
83
+ const executeTools = await client.discovery.search({
84
+ query: "whale transactions",
85
+ mode: "execute",
86
+ surface: "execute",
87
+ requireExecutePricing: true,
88
+ });
89
+
82
90
  const result = await client.tools.execute({
83
- toolId: "tool-uuid",
84
- toolName: "whale_transactions",
91
+ toolId: executeTools[0].id,
92
+ toolName: executeTools[0].mcpTools[0].name,
85
93
  args: { chain: "base", limit: 20 },
94
+ sessionId: session.session.sessionId ?? undefined,
86
95
  });
96
+ console.log(result.session); // methodPrice, spent, remaining, maxSpend, ...
87
97
  ```
88
98
 
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, completeness checks, model-aware context budgeting, and AI synthesis for one flat fee:
99
+ **Query mode** gives you curated answers — the server handles answer-safe tool discovery, multi-tool orchestration (up to 100 MCP calls per response turn), self-healing retries, completeness checks, model-aware context budgeting, and AI synthesis for one flat fee:
90
100
  ```typescript
91
101
  const answer = await client.query.run({
92
102
  query: "What are the top whale movements on Base?",
93
103
  modelId: "glm-model", // optional: choose a supported model
104
+ queryDepth: "auto", // optional: fast | auto | deep
94
105
  includeDataUrl: true, // optional: persist full execution data to blob
95
106
  });
96
107
  console.log(answer.response); // AI-synthesized answer
@@ -99,6 +110,11 @@ console.log(answer.cost); // Cost breakdown
99
110
  console.log(answer.dataUrl); // Optional blob URL with full data
100
111
  ```
101
112
 
113
+ > Mixed listings are first-class: one listing can expose methods to both surfaces. Methods without `_meta.pricing.executeUsd` remain query-only until priced.
114
+ >
115
+ > Compatibility: SDK/API payload fields such as `price` and `pricePerQuery` are retained for backward compatibility. In Query mode, they represent listing-level **price per response turn**.
116
+ > A future major release can add response-named aliases (for example, `pricePerResponse`) before deprecating legacy names.
117
+
102
118
  ## Quick Start
103
119
 
104
120
  ```typescript
@@ -112,16 +128,25 @@ const client = new ContextClient({
112
128
  const answer = await client.query.run("What are the top whale movements on Base?");
113
129
  console.log(answer.response);
114
130
 
115
- // Pay-per-request: Execute a specific tool for raw data
116
- const tools = await client.discovery.search("gas prices");
131
+ // Execute surface: require explicit execute pricing
132
+ const tools = await client.discovery.search({
133
+ query: "gas prices",
134
+ mode: "execute",
135
+ surface: "execute",
136
+ requireExecutePricing: true,
137
+ });
138
+ const session = await client.tools.startSession({ maxSpendUsd: "1.00" });
117
139
  const result = await client.tools.execute({
118
140
  toolId: tools[0].id,
119
141
  toolName: tools[0].mcpTools[0].name,
120
142
  args: { chainId: 1 },
143
+ sessionId: session.session.sessionId ?? undefined,
121
144
  });
122
145
  console.log(result.result);
123
146
  ```
124
147
 
148
+ See the runnable dual-surface example in [`examples/client/src/index.ts`](./examples/client/src/index.ts).
149
+
125
150
  ---
126
151
 
127
152
  ## The Agentic Pattern: How to Build Autonomous Bots
@@ -274,10 +299,12 @@ If you can answer without a tool, just respond normally.`;
274
299
 
275
300
  ### Client Options
276
301
 
277
- | Option | Type | Required | Default | Description |
278
- | --------- | -------- | -------- | ------------------------ | ------------------------------ |
279
- | `apiKey` | `string` | Yes | — | Your Context Protocol API key |
280
- | `baseUrl` | `string` | No | `https://ctxprotocol.com`| API base URL (for development) |
302
+ | Option | Type | Required | Default | Description |
303
+ | ------------------ | -------- | -------- | ------------------------------ | --------------------------------------------- |
304
+ | `apiKey` | `string` | Yes | — | Your Context Protocol API key |
305
+ | `baseUrl` | `string` | No | `https://www.ctxprotocol.com` | API base URL (for development) |
306
+ | `requestTimeoutMs` | `number` | No | `300000` | Timeout for non-streaming API calls |
307
+ | `streamTimeoutMs` | `number` | No | `600000` | Timeout for establishing streaming API calls |
281
308
 
282
309
  ```typescript
283
310
  // Production
@@ -289,6 +316,8 @@ const client = new ContextClient({
289
316
  const client = new ContextClient({
290
317
  apiKey: "sk_test_...",
291
318
  baseUrl: "http://localhost:3000",
319
+ requestTimeoutMs: 420_000,
320
+ streamTimeoutMs: 840_000,
292
321
  });
293
322
  ```
294
323
 
@@ -297,40 +326,84 @@ const client = new ContextClient({
297
326
  ### Discovery
298
327
 
299
328
  #### `client.discovery.search(query, limit?)`
329
+ #### `client.discovery.search(options)`
300
330
 
301
- Search for tools matching a query string.
331
+ Search for tools with optional surface-aware filters.
302
332
 
303
333
  ```typescript
304
334
  const tools = await client.discovery.search("ethereum gas", 10);
335
+
336
+ const executeTools = await client.discovery.search({
337
+ query: "ethereum gas",
338
+ mode: "execute",
339
+ surface: "execute",
340
+ requireExecutePricing: true,
341
+ });
305
342
  ```
306
343
 
307
- #### `client.discovery.getFeatured(limit?)`
344
+ #### `client.discovery.getFeatured(limit?, options?)`
308
345
 
309
346
  Get featured/popular tools.
310
347
 
311
348
  ```typescript
312
349
  const featured = await client.discovery.getFeatured(5);
350
+ const featuredExecute = await client.discovery.getFeatured(5, {
351
+ mode: "execute",
352
+ requireExecutePricing: true,
353
+ });
313
354
  ```
314
355
 
315
- ### Tools (Pay-Per-Request)
356
+ ### Tools (Execute Surface)
357
+
358
+ Session lifecycle helpers use the canonical execute-scoped API contract:
359
+ `/api/v1/tools/execute/sessions...`
316
360
 
317
361
  #### `client.tools.execute(options)`
318
362
 
319
- Execute a single tool method. One call, one payment, raw result.
363
+ Execute a single tool method. Execute calls can run inside session budgets.
320
364
 
321
365
  ```typescript
366
+ const session = await client.tools.startSession({ maxSpendUsd: "2.50" });
367
+
322
368
  const result = await client.tools.execute({
323
369
  toolId: "uuid-of-tool",
324
370
  toolName: "get_gas_prices",
325
371
  args: { chainId: 1 },
372
+ sessionId: session.session.sessionId ?? undefined,
326
373
  });
374
+
375
+ console.log(result.method.executePriceUsd);
376
+ console.log(result.session);
377
+ ```
378
+
379
+ #### `client.tools.startSession({ maxSpendUsd })`
380
+
381
+ ```typescript
382
+ const started = await client.tools.startSession({ maxSpendUsd: "5.00" });
383
+ ```
384
+
385
+ #### `client.tools.getSession(sessionId)`
386
+
387
+ ```typescript
388
+ const status = await client.tools.getSession("sess_123");
389
+ ```
390
+
391
+ #### `client.tools.closeSession(sessionId)`
392
+
393
+ ```typescript
394
+ const closed = await client.tools.closeSession("sess_123");
327
395
  ```
328
396
 
329
397
  ### Query (Pay-Per-Response)
330
398
 
331
399
  #### `client.query.run(options)`
332
400
 
333
- Run an agentic query. The server discovers tools, executes the full pipeline (up to 100 MCP calls per tool), applies model-aware mediator/data budgeting, and returns an AI-synthesized answer.
401
+ Run an agentic query. The server discovers answer-safe tools, executes the full pipeline (up to 100 MCP calls per response turn), applies model-aware mediator/data budgeting, and returns an AI-synthesized answer.
402
+
403
+ `queryDepth` controls orchestration depth:
404
+ - `fast`: lower-latency path for simple lookups.
405
+ - `auto`: server routes to either `fast` or `deep` from query intent + selected tool complexity.
406
+ - `deep`: completeness-oriented path (default when omitted).
334
407
 
335
408
  ```typescript
336
409
  // Simple string
@@ -341,6 +414,7 @@ const answer = await client.query.run({
341
414
  query: "Analyze whale activity on Base",
342
415
  tools: ["tool-uuid-1", "tool-uuid-2"], // optional — auto-discover if omitted
343
416
  modelId: "kimi-model-thinking", // optional
417
+ queryDepth: "auto", // optional: fast | auto | deep
344
418
  includeData: true, // optional: include execution data inline
345
419
  includeDataUrl: true, // optional: include blob URL for full data
346
420
  });
@@ -353,12 +427,17 @@ console.log(answer.data); // Optional execution data (when includeData=t
353
427
  console.log(answer.dataUrl); // Optional blob URL (when includeDataUrl=true)
354
428
  ```
355
429
 
430
+ When retrieval-first synthesis rollout is enabled server-side, full-data or truncation-sensitive query requests can switch to retrieval-first context assembly using private stage artifacts and canonical execution data slices. `includeData` and `includeDataUrl` continue to reference the same canonical dataset used for synthesis.
431
+
356
432
  #### `client.query.stream(options)`
357
433
 
358
434
  Same as `run()` but streams events in real-time via SSE.
359
435
 
360
436
  ```typescript
361
- for await (const event of client.query.stream("What are the top whale movements?")) {
437
+ for await (const event of client.query.stream({
438
+ query: "What are the top whale movements?",
439
+ queryDepth: "fast",
440
+ })) {
362
441
  switch (event.type) {
363
442
  case "tool-status":
364
443
  console.log(`Tool ${event.tool.name}: ${event.status}`);
@@ -414,7 +493,7 @@ interface Tool {
414
493
  id: string;
415
494
  name: string;
416
495
  description: string;
417
- price: string;
496
+ price: string; // listing-level response price metadata (legacy field name)
418
497
  category?: string;
419
498
  isVerified?: boolean;
420
499
  mcpTools?: McpTool[];
@@ -427,17 +506,35 @@ interface Tool {
427
506
  interface McpTool {
428
507
  name: string;
429
508
  description: string;
430
- inputSchema?: Record<string, unknown>; // JSON Schema for arguments
431
- outputSchema?: Record<string, unknown>; // JSON Schema for response
509
+ inputSchema?: Record<string, unknown>; // JSON Schema for arguments
510
+ outputSchema?: Record<string, unknown>; // JSON Schema for response
511
+ _meta?: {
512
+ surface?: "answer" | "execute" | "both";
513
+ queryEligible?: boolean;
514
+ latencyClass?: "instant" | "fast" | "slow" | "streaming";
515
+ pricing?: { executeUsd?: string; queryUsd?: string };
516
+ };
517
+ executeEligible?: boolean;
518
+ executePriceUsd?: string | null;
432
519
  }
433
520
  ```
434
521
 
435
- ### ExecutionResult (Pay-Per-Request)
522
+ ### ExecutionResult (Execute Surface)
436
523
 
437
524
  ```typescript
438
525
  interface ExecutionResult<T = unknown> {
526
+ mode: "execute";
439
527
  result: T;
440
528
  tool: { id: string; name: string };
529
+ method: { name: string; executePriceUsd: string };
530
+ session: {
531
+ sessionId: string | null;
532
+ methodPrice: string;
533
+ spent: string;
534
+ remaining: string | null;
535
+ maxSpend: string | null;
536
+ status?: "open" | "closed" | "expired";
537
+ };
441
538
  durationMs: number;
442
539
  }
443
540
  ```
@@ -832,13 +929,13 @@ pnpm add -D @types/express
832
929
 
833
930
  ## Payment Flow
834
931
 
835
- Context uses **deferred settlement** — tools execute first, and payment is settled in the background after the result is returned:
932
+ Context uses surface-aware deferred settlement:
836
933
 
837
- 1. Your USDC spending cap (ERC-20 allowance on ContextRouter) is verified
838
- 2. Tool executes and returns results
839
- 3. Payment is settled server-side via the operator wallet (no client-side transactions)
934
+ 1. **Query surface** settles after each response turn
935
+ 2. **Execute surface** accrues per-call spend into execute sessions, then flushes batches
936
+ 3. Your USDC spending cap (ERC-20 allowance on ContextRouter) is still the global ceiling
840
937
  4. **90%** goes to the tool developer, **10%** goes to the protocol
841
- 5. If execution fails, tool fees are **waived** you only pay for successful results
938
+ 5. Session responses expose `methodPrice`, `spent`, `remaining`, and `maxSpend` on each execute call
842
939
 
843
940
  ## Documentation
844
941
 
@@ -17,27 +17,36 @@ var Discovery = class {
17
17
  constructor(client) {
18
18
  this.client = client;
19
19
  }
20
- /**
21
- * Search for tools matching a query string
22
- *
23
- * @param query - The search query (e.g., "gas prices", "nft metadata")
24
- * @param limit - Maximum number of results (1-50, default 10)
25
- * @returns Array of matching tools
26
- *
27
- * @example
28
- * ```typescript
29
- * const tools = await client.discovery.search("gas prices");
30
- * console.log(tools[0].name); // "Gas Price Oracle"
31
- * console.log(tools[0].mcpTools); // Available methods
32
- * ```
33
- */
34
- async search(query, limit) {
20
+ async search(queryOrOptions, limit) {
21
+ const options = typeof queryOrOptions === "string" ? { query: queryOrOptions, limit } : queryOrOptions;
35
22
  const params = new URLSearchParams();
23
+ const query = options.query ?? "";
36
24
  if (query) {
37
25
  params.set("q", query);
38
26
  }
39
- if (limit !== void 0) {
40
- params.set("limit", String(limit));
27
+ if (options.limit !== void 0) {
28
+ params.set("limit", String(options.limit));
29
+ }
30
+ if (options.mode) {
31
+ params.set("mode", options.mode);
32
+ }
33
+ if (options.surface) {
34
+ params.set("surface", options.surface);
35
+ }
36
+ if (options.queryEligible !== void 0) {
37
+ params.set("queryEligible", String(options.queryEligible));
38
+ }
39
+ if (options.requireExecutePricing !== void 0) {
40
+ params.set(
41
+ "requireExecutePricing",
42
+ String(options.requireExecutePricing)
43
+ );
44
+ }
45
+ if (options.excludeLatencyClasses && options.excludeLatencyClasses.length > 0) {
46
+ params.set("excludeLatency", options.excludeLatencyClasses.join(","));
47
+ }
48
+ if (options.excludeSlow !== void 0) {
49
+ params.set("excludeSlow", String(options.excludeSlow));
41
50
  }
42
51
  const queryString = params.toString();
43
52
  const endpoint = `/api/v1/tools/search${queryString ? `?${queryString}` : ""}`;
@@ -55,8 +64,12 @@ var Discovery = class {
55
64
  * const featured = await client.discovery.getFeatured(5);
56
65
  * ```
57
66
  */
58
- async getFeatured(limit) {
59
- return this.search("", limit);
67
+ async getFeatured(limit, options) {
68
+ return this.search({
69
+ ...options ?? {},
70
+ query: "",
71
+ ...limit !== void 0 ? { limit } : {}
72
+ });
60
73
  }
61
74
  };
62
75
 
@@ -97,14 +110,31 @@ var Tools = class {
97
110
  * ```
98
111
  */
99
112
  async execute(options) {
100
- const { toolId, toolName, args, idempotencyKey } = options;
113
+ const {
114
+ toolId,
115
+ toolName,
116
+ args,
117
+ idempotencyKey,
118
+ mode,
119
+ sessionId,
120
+ maxSpendUsd,
121
+ closeSession
122
+ } = options;
101
123
  const headers = idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0;
102
124
  const response = await this.client._fetch(
103
125
  "/api/v1/tools/execute",
104
126
  {
105
127
  method: "POST",
106
128
  headers,
107
- body: JSON.stringify({ toolId, toolName, args })
129
+ body: JSON.stringify({
130
+ toolId,
131
+ toolName,
132
+ args,
133
+ mode: mode ?? "execute",
134
+ sessionId,
135
+ maxSpendUsd,
136
+ closeSession
137
+ })
108
138
  }
109
139
  );
110
140
  if ("error" in response) {
@@ -118,13 +148,79 @@ var Tools = class {
118
148
  }
119
149
  if (response.success) {
120
150
  return {
151
+ mode: response.mode,
121
152
  result: response.result,
122
153
  tool: response.tool,
154
+ method: response.method,
155
+ session: response.session,
123
156
  durationMs: response.durationMs
124
157
  };
125
158
  }
126
159
  throw new ContextError("Unexpected response format from API");
127
160
  }
161
+ /**
162
+ * Start an execute session with a max spend budget.
163
+ */
164
+ async startSession(options) {
165
+ const response = await this.client._fetch(
166
+ "/api/v1/tools/execute/sessions",
167
+ {
168
+ method: "POST",
169
+ body: JSON.stringify({
170
+ mode: "execute",
171
+ maxSpendUsd: options.maxSpendUsd
172
+ })
173
+ }
174
+ );
175
+ return this.resolveSessionLifecycleResponse(response);
176
+ }
177
+ /**
178
+ * Fetch current execute session status by ID.
179
+ */
180
+ async getSession(sessionId) {
181
+ if (!sessionId) {
182
+ throw new ContextError("sessionId is required");
183
+ }
184
+ const encodedSessionId = encodeURIComponent(sessionId);
185
+ const response = await this.client._fetch(
186
+ `/api/v1/tools/execute/sessions/${encodedSessionId}`
187
+ );
188
+ return this.resolveSessionLifecycleResponse(response);
189
+ }
190
+ /**
191
+ * Close an execute session by ID.
192
+ */
193
+ async closeSession(sessionId) {
194
+ if (!sessionId) {
195
+ throw new ContextError("sessionId is required");
196
+ }
197
+ const encodedSessionId = encodeURIComponent(sessionId);
198
+ const response = await this.client._fetch(
199
+ `/api/v1/tools/execute/sessions/${encodedSessionId}/close`,
200
+ {
201
+ method: "POST",
202
+ body: JSON.stringify({ mode: "execute" })
203
+ }
204
+ );
205
+ return this.resolveSessionLifecycleResponse(response);
206
+ }
207
+ resolveSessionLifecycleResponse(response) {
208
+ if ("error" in response) {
209
+ throw new ContextError(
210
+ response.error,
211
+ response.code,
212
+ void 0,
213
+ response.helpUrl
214
+ );
215
+ }
216
+ if (response.success) {
217
+ return {
218
+ mode: response.mode,
219
+ session: response.session
220
+ };
221
+ }
222
+ throw new ContextError("Unexpected response format from API");
223
+ }
128
224
  };
129
225
 
130
226
  // src/client/resources/query.ts
@@ -177,6 +273,7 @@ var Query = class {
177
273
  modelId: opts.modelId,
178
274
  includeData: opts.includeData,
179
275
  includeDataUrl: opts.includeDataUrl,
276
+ queryDepth: opts.queryDepth,
180
277
  stream: false
181
278
  })
182
279
  }
@@ -242,6 +339,7 @@ var Query = class {
242
339
  modelId: opts.modelId,
243
340
  includeData: opts.includeData,
244
341
  includeDataUrl: opts.includeDataUrl,
342
+ queryDepth: opts.queryDepth,
245
343
  stream: true
246
344
  })
247
345
  });
@@ -287,9 +385,14 @@ var Query = class {
287
385
  };
288
386
 
289
387
  // src/client/client.ts
388
+ var DEFAULT_BASE_URL = "https://www.ctxprotocol.com";
389
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e5;
390
+ var DEFAULT_STREAM_TIMEOUT_MS = 6e5;
290
391
  var ContextClient = class {
291
392
  apiKey;
292
393
  baseUrl;
394
+ requestTimeoutMs;
395
+ streamTimeoutMs;
293
396
  _closed = false;
294
397
  /**
295
398
  * Discovery resource for searching tools
@@ -312,14 +415,26 @@ var ContextClient = class {
312
415
  *
313
416
  * @param options - Client configuration options
314
417
  * @param options.apiKey - Your Context Protocol API key (format: sk_live_...)
315
- * @param options.baseUrl - Optional base URL override (defaults to https://ctxprotocol.com)
418
+ * @param options.baseUrl - Optional base URL override (defaults to https://www.ctxprotocol.com)
419
+ * @param options.requestTimeoutMs - Optional timeout for non-streaming requests (default 300000ms)
420
+ * @param options.streamTimeoutMs - Optional timeout for establishing stream requests (default 600000ms)
316
421
  */
317
422
  constructor(options) {
318
423
  if (!options.apiKey) {
319
424
  throw new ContextError("API key is required");
320
425
  }
426
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
427
+ const streamTimeoutMs = options.streamTimeoutMs ?? DEFAULT_STREAM_TIMEOUT_MS;
428
+ if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) {
429
+ throw new ContextError("requestTimeoutMs must be a positive number");
430
+ }
431
+ if (!Number.isFinite(streamTimeoutMs) || streamTimeoutMs <= 0) {
432
+ throw new ContextError("streamTimeoutMs must be a positive number");
433
+ }
321
434
  this.apiKey = options.apiKey;
322
- this.baseUrl = (options.baseUrl ?? "https://ctxprotocol.com").replace(/\/$/, "");
435
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
436
+ this.requestTimeoutMs = requestTimeoutMs;
437
+ this.streamTimeoutMs = streamTimeoutMs;
323
438
  this.discovery = new Discovery(this);
324
439
  this.tools = new Tools(this);
325
440
  this.query = new Query(this);
@@ -333,7 +448,7 @@ var ContextClient = class {
333
448
  }
334
449
  /**
335
450
  * Internal method for making authenticated HTTP requests
336
- * Includes timeout (30s) and retry with exponential backoff for transient errors
451
+ * Includes timeout and retry with exponential backoff for transient errors
337
452
  *
338
453
  * @internal
339
454
  */
@@ -343,7 +458,7 @@ var ContextClient = class {
343
458
  }
344
459
  const url = `${this.baseUrl}${endpoint}`;
345
460
  const maxRetries = 3;
346
- const timeoutMs = 3e4;
461
+ const timeoutMs = this.requestTimeoutMs;
347
462
  let lastError;
348
463
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
349
464
  const controller = new AbortController();
@@ -411,6 +526,7 @@ var ContextClient = class {
411
526
  /**
412
527
  * Internal method for making authenticated HTTP requests that returns
413
528
  * the raw Response object. Used for streaming endpoints (SSE).
529
+ * Includes a configurable timeout for stream setup.
414
530
  *
415
531
  * @internal
416
532
  */
@@ -419,14 +535,32 @@ var ContextClient = class {
419
535
  throw new ContextError("Client has been closed");
420
536
  }
421
537
  const url = `${this.baseUrl}${endpoint}`;
422
- const response = await fetch(url, {
423
- ...options,
424
- headers: {
425
- "Content-Type": "application/json",
426
- Authorization: `Bearer ${this.apiKey}`,
427
- ...options.headers
538
+ const controller = new AbortController();
539
+ const timeout = setTimeout(() => controller.abort(), this.streamTimeoutMs);
540
+ let response;
541
+ try {
542
+ response = await fetch(url, {
543
+ ...options,
544
+ signal: controller.signal,
545
+ headers: {
546
+ "Content-Type": "application/json",
547
+ Authorization: `Bearer ${this.apiKey}`,
548
+ ...options.headers
549
+ }
550
+ });
551
+ } catch (error) {
552
+ clearTimeout(timeout);
553
+ const lastError = error instanceof Error ? error : new Error(String(error));
554
+ if (lastError.name === "AbortError") {
555
+ throw new ContextError(
556
+ `Streaming request timed out after ${this.streamTimeoutMs / 1e3}s`,
557
+ void 0,
558
+ 408
559
+ );
428
560
  }
429
- });
561
+ throw new ContextError(lastError.message);
562
+ }
563
+ clearTimeout(timeout);
430
564
  if (!response.ok) {
431
565
  let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
432
566
  let errorCode;