@ctxprotocol/sdk 0.8.0 → 0.8.2

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,21 +72,31 @@ 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?",
@@ -99,6 +109,11 @@ console.log(answer.cost); // Cost breakdown
99
109
  console.log(answer.dataUrl); // Optional blob URL with full data
100
110
  ```
101
111
 
112
+ > Mixed listings are first-class: one listing can expose methods to both surfaces. Methods without `_meta.pricing.executeUsd` remain query-only until priced.
113
+ >
114
+ > 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**.
115
+ > A future major release can add response-named aliases (for example, `pricePerResponse`) before deprecating legacy names.
116
+
102
117
  ## Quick Start
103
118
 
104
119
  ```typescript
@@ -112,16 +127,25 @@ const client = new ContextClient({
112
127
  const answer = await client.query.run("What are the top whale movements on Base?");
113
128
  console.log(answer.response);
114
129
 
115
- // Pay-per-request: Execute a specific tool for raw data
116
- const tools = await client.discovery.search("gas prices");
130
+ // Execute surface: require explicit execute pricing
131
+ const tools = await client.discovery.search({
132
+ query: "gas prices",
133
+ mode: "execute",
134
+ surface: "execute",
135
+ requireExecutePricing: true,
136
+ });
137
+ const session = await client.tools.startSession({ maxSpendUsd: "1.00" });
117
138
  const result = await client.tools.execute({
118
139
  toolId: tools[0].id,
119
140
  toolName: tools[0].mcpTools[0].name,
120
141
  args: { chainId: 1 },
142
+ sessionId: session.session.sessionId ?? undefined,
121
143
  });
122
144
  console.log(result.result);
123
145
  ```
124
146
 
147
+ See the runnable dual-surface example in [`examples/client/src/index.ts`](./examples/client/src/index.ts).
148
+
125
149
  ---
126
150
 
127
151
  ## The Agentic Pattern: How to Build Autonomous Bots
@@ -297,40 +321,79 @@ const client = new ContextClient({
297
321
  ### Discovery
298
322
 
299
323
  #### `client.discovery.search(query, limit?)`
324
+ #### `client.discovery.search(options)`
300
325
 
301
- Search for tools matching a query string.
326
+ Search for tools with optional surface-aware filters.
302
327
 
303
328
  ```typescript
304
329
  const tools = await client.discovery.search("ethereum gas", 10);
330
+
331
+ const executeTools = await client.discovery.search({
332
+ query: "ethereum gas",
333
+ mode: "execute",
334
+ surface: "execute",
335
+ requireExecutePricing: true,
336
+ });
305
337
  ```
306
338
 
307
- #### `client.discovery.getFeatured(limit?)`
339
+ #### `client.discovery.getFeatured(limit?, options?)`
308
340
 
309
341
  Get featured/popular tools.
310
342
 
311
343
  ```typescript
312
344
  const featured = await client.discovery.getFeatured(5);
345
+ const featuredExecute = await client.discovery.getFeatured(5, {
346
+ mode: "execute",
347
+ requireExecutePricing: true,
348
+ });
313
349
  ```
314
350
 
315
- ### Tools (Pay-Per-Request)
351
+ ### Tools (Execute Surface)
352
+
353
+ Session lifecycle helpers use the canonical execute-scoped API contract:
354
+ `/api/v1/tools/execute/sessions...`
316
355
 
317
356
  #### `client.tools.execute(options)`
318
357
 
319
- Execute a single tool method. One call, one payment, raw result.
358
+ Execute a single tool method. Execute calls can run inside session budgets.
320
359
 
321
360
  ```typescript
361
+ const session = await client.tools.startSession({ maxSpendUsd: "2.50" });
362
+
322
363
  const result = await client.tools.execute({
323
364
  toolId: "uuid-of-tool",
324
365
  toolName: "get_gas_prices",
325
366
  args: { chainId: 1 },
367
+ sessionId: session.session.sessionId ?? undefined,
326
368
  });
369
+
370
+ console.log(result.method.executePriceUsd);
371
+ console.log(result.session);
372
+ ```
373
+
374
+ #### `client.tools.startSession({ maxSpendUsd })`
375
+
376
+ ```typescript
377
+ const started = await client.tools.startSession({ maxSpendUsd: "5.00" });
378
+ ```
379
+
380
+ #### `client.tools.getSession(sessionId)`
381
+
382
+ ```typescript
383
+ const status = await client.tools.getSession("sess_123");
384
+ ```
385
+
386
+ #### `client.tools.closeSession(sessionId)`
387
+
388
+ ```typescript
389
+ const closed = await client.tools.closeSession("sess_123");
327
390
  ```
328
391
 
329
392
  ### Query (Pay-Per-Response)
330
393
 
331
394
  #### `client.query.run(options)`
332
395
 
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.
396
+ 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.
334
397
 
335
398
  ```typescript
336
399
  // Simple string
@@ -414,7 +477,7 @@ interface Tool {
414
477
  id: string;
415
478
  name: string;
416
479
  description: string;
417
- price: string;
480
+ price: string; // listing-level response price metadata (legacy field name)
418
481
  category?: string;
419
482
  isVerified?: boolean;
420
483
  mcpTools?: McpTool[];
@@ -427,17 +490,35 @@ interface Tool {
427
490
  interface McpTool {
428
491
  name: string;
429
492
  description: string;
430
- inputSchema?: Record<string, unknown>; // JSON Schema for arguments
431
- outputSchema?: Record<string, unknown>; // JSON Schema for response
493
+ inputSchema?: Record<string, unknown>; // JSON Schema for arguments
494
+ outputSchema?: Record<string, unknown>; // JSON Schema for response
495
+ _meta?: {
496
+ surface?: "answer" | "execute" | "both";
497
+ queryEligible?: boolean;
498
+ latencyClass?: "instant" | "fast" | "slow" | "streaming";
499
+ pricing?: { executeUsd?: string; queryUsd?: string };
500
+ };
501
+ executeEligible?: boolean;
502
+ executePriceUsd?: string | null;
432
503
  }
433
504
  ```
434
505
 
435
- ### ExecutionResult (Pay-Per-Request)
506
+ ### ExecutionResult (Execute Surface)
436
507
 
437
508
  ```typescript
438
509
  interface ExecutionResult<T = unknown> {
510
+ mode: "execute";
439
511
  result: T;
440
512
  tool: { id: string; name: string };
513
+ method: { name: string; executePriceUsd: string };
514
+ session: {
515
+ sessionId: string | null;
516
+ methodPrice: string;
517
+ spent: string;
518
+ remaining: string | null;
519
+ maxSpend: string | null;
520
+ status?: "open" | "closed" | "expired";
521
+ };
441
522
  durationMs: number;
442
523
  }
443
524
  ```
@@ -701,10 +782,19 @@ const TOOLS = [{
701
782
  name: "analyze_my_positions",
702
783
  description: "Analyze your positions with personalized insights",
703
784
 
704
- // ⭐ REQUIRED: Context requirements in _meta (MCP spec for arbitrary metadata)
705
- // The Context platform reads this to inject user data
785
+ // ⭐ `_meta` is standard MCP metadata:
786
+ // - contextRequirements => context injection contract
787
+ // - rateLimit => planner/runtime pacing hints for agentic loops
706
788
  _meta: {
707
789
  contextRequirements: ["wallet"] as ContextRequirementType[],
790
+ rateLimit: {
791
+ maxRequestsPerMinute: 30,
792
+ cooldownMs: 2000,
793
+ maxConcurrency: 1,
794
+ supportsBulk: true,
795
+ recommendedBatchTools: ["get_wallet_snapshot"],
796
+ notes: "Hobby tier: prefer snapshot endpoints over fan-out loops.",
797
+ },
708
798
  },
709
799
 
710
800
  inputSchema: {
@@ -723,7 +813,11 @@ const TOOLS = [{
723
813
 
724
814
  **Why `_meta` at the tool level?**
725
815
 
726
- The `_meta` field is part of the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-definition) for arbitrary tool metadata. The Context platform reads `_meta.contextRequirements` to determine what user data to inject. This is preserved through MCP transport because it's a standard field.
816
+ The `_meta` field is part of the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-definition) for arbitrary tool metadata. The Context platform reads:
817
+ - `_meta.contextRequirements` for user-context injection
818
+ - `_meta.rateLimit` / `_meta.rateLimitHints` for planner + runtime pacing guidance
819
+
820
+ Because `_meta` is an MCP-standard field, these hints survive normal MCP transport and discovery.
727
821
 
728
822
  **Available context types:**
729
823
 
@@ -819,13 +913,13 @@ pnpm add -D @types/express
819
913
 
820
914
  ## Payment Flow
821
915
 
822
- Context uses **deferred settlement** — tools execute first, and payment is settled in the background after the result is returned:
916
+ Context uses surface-aware deferred settlement:
823
917
 
824
- 1. Your USDC spending cap (ERC-20 allowance on ContextRouter) is verified
825
- 2. Tool executes and returns results
826
- 3. Payment is settled server-side via the operator wallet (no client-side transactions)
918
+ 1. **Query surface** settles after each response turn
919
+ 2. **Execute surface** accrues per-call spend into execute sessions, then flushes batches
920
+ 3. Your USDC spending cap (ERC-20 allowance on ContextRouter) is still the global ceiling
827
921
  4. **90%** goes to the tool developer, **10%** goes to the protocol
828
- 5. If execution fails, tool fees are **waived** you only pay for successful results
922
+ 5. Session responses expose `methodPrice`, `spent`, `remaining`, and `maxSpend` on each execute call
829
923
 
830
924
  ## Documentation
831
925
 
@@ -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