@ctxprotocol/sdk 0.5.5 → 0.6.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 +123 -47
- package/dist/client/index.cjs +80 -26
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +20 -6
- package/dist/client/index.d.ts +20 -6
- package/dist/client/index.js +80 -26
- package/dist/client/index.js.map +1 -1
- package/dist/index.cjs +176 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +270 -16
- package/dist/index.d.ts +270 -16
- package/dist/index.js +169 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -90,8 +90,14 @@ interface PolymarketOrder {
|
|
|
90
90
|
* This is what gets passed to MCP tools for personalized analysis.
|
|
91
91
|
*/
|
|
92
92
|
interface PolymarketContext {
|
|
93
|
-
/** The wallet address this context is for */
|
|
93
|
+
/** The wallet address this context is for (may be comma-separated if multiple) */
|
|
94
94
|
walletAddress: string;
|
|
95
|
+
/**
|
|
96
|
+
* The active wallet address for signing (the one with Polymarket activity).
|
|
97
|
+
* When multiple wallets are linked, this is the wallet that should be used
|
|
98
|
+
* for placing orders. Determined by activity detection on the client.
|
|
99
|
+
*/
|
|
100
|
+
activeWalletAddress?: string;
|
|
95
101
|
/** All open positions */
|
|
96
102
|
positions: PolymarketPosition[];
|
|
97
103
|
/** All open orders */
|
|
@@ -228,9 +234,11 @@ interface HyperliquidContext {
|
|
|
228
234
|
* DECLARING CONTEXT REQUIREMENTS
|
|
229
235
|
* =============================================================================
|
|
230
236
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
237
|
+
* Context requirements are declared via `_meta.contextRequirements` at the tool level.
|
|
238
|
+
* This is the primary mechanism that the Context Platform reads.
|
|
239
|
+
*
|
|
240
|
+
* Previously, `x-context-requirements` in inputSchema was recommended, but the MCP SDK
|
|
241
|
+
* may strip extension properties during transport. Use `_meta` instead.
|
|
234
242
|
*
|
|
235
243
|
* @example
|
|
236
244
|
* ```typescript
|
|
@@ -239,9 +247,11 @@ interface HyperliquidContext {
|
|
|
239
247
|
*
|
|
240
248
|
* const tool = {
|
|
241
249
|
* name: "analyze_my_positions",
|
|
250
|
+
* _meta: {
|
|
251
|
+
* contextRequirements: ["hyperliquid"] as ContextRequirementType[],
|
|
252
|
+
* },
|
|
242
253
|
* inputSchema: {
|
|
243
254
|
* type: "object",
|
|
244
|
-
* [CONTEXT_REQUIREMENTS_KEY]: ["hyperliquid"] as ContextRequirementType[],
|
|
245
255
|
* properties: {
|
|
246
256
|
* portfolio: { type: "object" }
|
|
247
257
|
* },
|
|
@@ -260,30 +270,33 @@ interface HyperliquidContext {
|
|
|
260
270
|
*/
|
|
261
271
|
|
|
262
272
|
/**
|
|
263
|
-
*
|
|
273
|
+
* @deprecated Use `_meta.contextRequirements` instead (see META_CONTEXT_REQUIREMENTS_KEY).
|
|
264
274
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
* This key was designed for embedding requirements in inputSchema,
|
|
276
|
+
* but the MCP SDK may strip `x-` prefixed extension properties during transport.
|
|
277
|
+
* The `_meta.contextRequirements` approach is what the Context Platform reads.
|
|
278
|
+
*/
|
|
279
|
+
declare const CONTEXT_REQUIREMENTS_KEY: "x-context-requirements";
|
|
280
|
+
/**
|
|
281
|
+
* The key used inside `_meta` to declare context requirements.
|
|
282
|
+
* This is the PRIMARY mechanism — the Context Platform reads `_meta.contextRequirements`.
|
|
270
283
|
*
|
|
271
284
|
* @example
|
|
272
285
|
* ```typescript
|
|
273
|
-
* import { CONTEXT_REQUIREMENTS_KEY } from "@ctxprotocol/sdk";
|
|
274
|
-
*
|
|
275
286
|
* const tool = {
|
|
276
287
|
* name: "analyze_my_positions",
|
|
288
|
+
* _meta: {
|
|
289
|
+
* [META_CONTEXT_REQUIREMENTS_KEY]: ["hyperliquid"] as ContextRequirementType[],
|
|
290
|
+
* },
|
|
277
291
|
* inputSchema: {
|
|
278
292
|
* type: "object",
|
|
279
|
-
* [CONTEXT_REQUIREMENTS_KEY]: ["hyperliquid"],
|
|
280
293
|
* properties: { portfolio: { type: "object" } },
|
|
281
294
|
* required: ["portfolio"]
|
|
282
295
|
* }
|
|
283
296
|
* };
|
|
284
297
|
* ```
|
|
285
298
|
*/
|
|
286
|
-
declare const
|
|
299
|
+
declare const META_CONTEXT_REQUIREMENTS_KEY: "contextRequirements";
|
|
287
300
|
/**
|
|
288
301
|
* Context requirement types supported by the Context marketplace.
|
|
289
302
|
* Maps to protocol-specific context builders on the platform.
|
|
@@ -434,4 +447,245 @@ interface CreateContextMiddlewareOptions {
|
|
|
434
447
|
*/
|
|
435
448
|
declare function createContextMiddleware(options?: CreateContextMiddlewareOptions): (req: ContextRequest, res: ContextResponse, next: NextFunction) => Promise<void>;
|
|
436
449
|
|
|
437
|
-
|
|
450
|
+
/**
|
|
451
|
+
* Handshake Types for MCP Tool Developers
|
|
452
|
+
*
|
|
453
|
+
* Use these types when your tool needs to request user interaction
|
|
454
|
+
* before completing an action (signatures, transactions, OAuth).
|
|
455
|
+
*
|
|
456
|
+
* @see https://docs.ctxprotocol.com/guides/handshake-architecture
|
|
457
|
+
*
|
|
458
|
+
* ## Usage Pattern
|
|
459
|
+
*
|
|
460
|
+
* Tools return handshake actions in the `_meta.handshakeAction` field
|
|
461
|
+
* of their MCP response. The Context platform intercepts these and
|
|
462
|
+
* presents the appropriate UI to the user.
|
|
463
|
+
*
|
|
464
|
+
* ## Action Types
|
|
465
|
+
*
|
|
466
|
+
* - `signature_request`: For EIP-712 signatures (Hyperliquid, Polymarket, etc.)
|
|
467
|
+
* - `transaction_proposal`: For direct on-chain transactions (Uniswap, NFT mints)
|
|
468
|
+
* - `auth_required`: For OAuth flows (Discord, Twitter, etc.)
|
|
469
|
+
*/
|
|
470
|
+
type HandshakeMeta = {
|
|
471
|
+
/** Human-readable description of the action */
|
|
472
|
+
description: string;
|
|
473
|
+
/** Protocol name (e.g., "Hyperliquid", "Polymarket") */
|
|
474
|
+
protocol?: string;
|
|
475
|
+
/** Action verb (e.g., "Place Order", "Place Bid") */
|
|
476
|
+
action?: string;
|
|
477
|
+
/** Token symbol if relevant */
|
|
478
|
+
tokenSymbol?: string;
|
|
479
|
+
/** Human-readable token amount */
|
|
480
|
+
tokenAmount?: string;
|
|
481
|
+
/** UI warning level */
|
|
482
|
+
warningLevel?: "info" | "caution" | "danger";
|
|
483
|
+
/** Custom title for the signature card (marketplace-friendly, overrides action-based title) */
|
|
484
|
+
title?: string;
|
|
485
|
+
/** Custom subtitle for the signature card (overrides tool name display) */
|
|
486
|
+
subtitle?: string;
|
|
487
|
+
};
|
|
488
|
+
type EIP712Domain = {
|
|
489
|
+
/** Domain name (e.g., "Hyperliquid", "ClobAuthDomain") */
|
|
490
|
+
name: string;
|
|
491
|
+
/** Domain version */
|
|
492
|
+
version: string;
|
|
493
|
+
/** Chain ID (informational - signing is chain-agnostic) */
|
|
494
|
+
chainId: number;
|
|
495
|
+
/** Optional verifying contract address */
|
|
496
|
+
verifyingContract?: `0x${string}`;
|
|
497
|
+
};
|
|
498
|
+
type EIP712TypeField = {
|
|
499
|
+
name: string;
|
|
500
|
+
type: string;
|
|
501
|
+
};
|
|
502
|
+
/**
|
|
503
|
+
* Signature Request
|
|
504
|
+
*
|
|
505
|
+
* Use this for platforms with proxy wallets (Hyperliquid, Polymarket, dYdX).
|
|
506
|
+
*
|
|
507
|
+
* Benefits:
|
|
508
|
+
* - No gas required (user signs a message, not a transaction)
|
|
509
|
+
* - No network switching needed (signing is chain-agnostic)
|
|
510
|
+
* - Works with Privy embedded wallets on any chain
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```typescript
|
|
514
|
+
* return {
|
|
515
|
+
* structuredContent: {
|
|
516
|
+
* _meta: {
|
|
517
|
+
* handshakeAction: createSignatureRequest({
|
|
518
|
+
* domain: { name: "Hyperliquid", version: "1", chainId: 42161 },
|
|
519
|
+
* types: { Order: [...] },
|
|
520
|
+
* primaryType: "Order",
|
|
521
|
+
* message: { asset: 4, isBuy: true, ... },
|
|
522
|
+
* meta: { description: "Place Long ETH order", protocol: "Hyperliquid" }
|
|
523
|
+
* })
|
|
524
|
+
* }
|
|
525
|
+
* }
|
|
526
|
+
* };
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
type SignatureRequest = {
|
|
530
|
+
_action: "signature_request";
|
|
531
|
+
/** EIP-712 domain separator */
|
|
532
|
+
domain: EIP712Domain;
|
|
533
|
+
/** EIP-712 type definitions */
|
|
534
|
+
types: Record<string, EIP712TypeField[]>;
|
|
535
|
+
/** The primary type being signed */
|
|
536
|
+
primaryType: string;
|
|
537
|
+
/** The message data to sign */
|
|
538
|
+
message: Record<string, unknown>;
|
|
539
|
+
/** UI metadata for the approval card */
|
|
540
|
+
meta?: HandshakeMeta;
|
|
541
|
+
/**
|
|
542
|
+
* Optional: Tool name to call with the signature result.
|
|
543
|
+
* If provided, the platform will call this tool with { signature, originalParams }
|
|
544
|
+
* after the user signs.
|
|
545
|
+
*/
|
|
546
|
+
callbackToolName?: string;
|
|
547
|
+
};
|
|
548
|
+
type TransactionProposalMeta = HandshakeMeta & {
|
|
549
|
+
/** Estimated gas cost (informational - Context may sponsor) */
|
|
550
|
+
estimatedGas?: string;
|
|
551
|
+
/** Link to contract on block explorer */
|
|
552
|
+
explorerUrl?: string;
|
|
553
|
+
};
|
|
554
|
+
/**
|
|
555
|
+
* Transaction Proposal
|
|
556
|
+
*
|
|
557
|
+
* Use this for protocols without proxy wallets (Uniswap, NFT mints, etc.).
|
|
558
|
+
*
|
|
559
|
+
* Note: May require network switching and gas fees.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```typescript
|
|
563
|
+
* return {
|
|
564
|
+
* structuredContent: {
|
|
565
|
+
* _meta: {
|
|
566
|
+
* handshakeAction: createTransactionProposal({
|
|
567
|
+
* chainId: 8453,
|
|
568
|
+
* to: "0x...",
|
|
569
|
+
* data: "0x...",
|
|
570
|
+
* meta: { description: "Swap 100 USDC for ETH", protocol: "Uniswap" }
|
|
571
|
+
* })
|
|
572
|
+
* }
|
|
573
|
+
* }
|
|
574
|
+
* };
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
type TransactionProposal = {
|
|
578
|
+
_action: "transaction_proposal";
|
|
579
|
+
/** EVM chain ID (e.g., 137 for Polygon, 8453 for Base) */
|
|
580
|
+
chainId: number;
|
|
581
|
+
/** Target contract address */
|
|
582
|
+
to: `0x${string}`;
|
|
583
|
+
/** Encoded calldata */
|
|
584
|
+
data: `0x${string}`;
|
|
585
|
+
/** Wei to send (as string, default "0") */
|
|
586
|
+
value?: string;
|
|
587
|
+
/** UI metadata for the approval card */
|
|
588
|
+
meta?: TransactionProposalMeta;
|
|
589
|
+
};
|
|
590
|
+
type AuthRequiredMeta = {
|
|
591
|
+
/** Human-friendly service name */
|
|
592
|
+
displayName?: string;
|
|
593
|
+
/** Permissions being requested */
|
|
594
|
+
scopes?: string[];
|
|
595
|
+
/** Description of what access is needed */
|
|
596
|
+
description?: string;
|
|
597
|
+
/** Tool's icon URL */
|
|
598
|
+
iconUrl?: string;
|
|
599
|
+
/** How long authorization lasts */
|
|
600
|
+
expiresIn?: string;
|
|
601
|
+
};
|
|
602
|
+
/**
|
|
603
|
+
* Auth Required
|
|
604
|
+
*
|
|
605
|
+
* Use this when your tool needs the user to authenticate with an external service.
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```typescript
|
|
609
|
+
* if (!hasUserToken(contextDid)) {
|
|
610
|
+
* return {
|
|
611
|
+
* structuredContent: {
|
|
612
|
+
* _meta: {
|
|
613
|
+
* handshakeAction: createAuthRequired({
|
|
614
|
+
* provider: "discord",
|
|
615
|
+
* authUrl: "https://your-server.com/oauth/discord",
|
|
616
|
+
* meta: { displayName: "Discord Bot", scopes: ["send_messages"] }
|
|
617
|
+
* })
|
|
618
|
+
* }
|
|
619
|
+
* }
|
|
620
|
+
* };
|
|
621
|
+
* }
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
type AuthRequired = {
|
|
625
|
+
_action: "auth_required";
|
|
626
|
+
/** Service identifier (e.g., "discord", "slack") */
|
|
627
|
+
provider: string;
|
|
628
|
+
/** Your OAuth initiation endpoint (MUST be HTTPS) */
|
|
629
|
+
authUrl: string;
|
|
630
|
+
/** UI metadata for the auth card */
|
|
631
|
+
meta?: AuthRequiredMeta;
|
|
632
|
+
};
|
|
633
|
+
type HandshakeAction = SignatureRequest | TransactionProposal | AuthRequired;
|
|
634
|
+
declare function isHandshakeAction(value: unknown): value is HandshakeAction;
|
|
635
|
+
declare function isSignatureRequest(value: unknown): value is SignatureRequest;
|
|
636
|
+
declare function isTransactionProposal(value: unknown): value is TransactionProposal;
|
|
637
|
+
declare function isAuthRequired(value: unknown): value is AuthRequired;
|
|
638
|
+
/**
|
|
639
|
+
* Create a signature request response.
|
|
640
|
+
* Return this from your tool when you need the user to sign EIP-712 typed data.
|
|
641
|
+
*
|
|
642
|
+
* Use this for platforms with proxy wallets (Hyperliquid, Polymarket, dYdX).
|
|
643
|
+
* Benefits: No gas required, no network switching needed.
|
|
644
|
+
*/
|
|
645
|
+
declare function createSignatureRequest(params: Omit<SignatureRequest, "_action">): SignatureRequest;
|
|
646
|
+
/**
|
|
647
|
+
* Create a transaction proposal response.
|
|
648
|
+
* Return this from your tool when you need the user to sign a direct on-chain transaction.
|
|
649
|
+
*
|
|
650
|
+
* Use this for protocols that don't use proxy wallets (Uniswap, NFT mints, etc.).
|
|
651
|
+
* Note: May require network switching and gas.
|
|
652
|
+
*/
|
|
653
|
+
declare function createTransactionProposal(params: Omit<TransactionProposal, "_action">): TransactionProposal;
|
|
654
|
+
/**
|
|
655
|
+
* Create an auth required response.
|
|
656
|
+
* Return this from your tool when you need the user to authenticate via OAuth.
|
|
657
|
+
*/
|
|
658
|
+
declare function createAuthRequired(params: Omit<AuthRequired, "_action">): AuthRequired;
|
|
659
|
+
/**
|
|
660
|
+
* Wrap a handshake action in the proper MCP response format.
|
|
661
|
+
*
|
|
662
|
+
* MCP tools should return handshake actions in `_meta.handshakeAction` to prevent
|
|
663
|
+
* the MCP SDK from stripping unknown fields.
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* ```typescript
|
|
667
|
+
* // In your tool handler:
|
|
668
|
+
* return wrapHandshakeResponse(createSignatureRequest({
|
|
669
|
+
* domain: { name: "Hyperliquid", version: "1", chainId: 42161 },
|
|
670
|
+
* types: { Order: [...] },
|
|
671
|
+
* primaryType: "Order",
|
|
672
|
+
* message: orderData,
|
|
673
|
+
* meta: { description: "Place order", protocol: "Hyperliquid" }
|
|
674
|
+
* }));
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
declare function wrapHandshakeResponse(action: HandshakeAction): {
|
|
678
|
+
content: Array<{
|
|
679
|
+
type: "text";
|
|
680
|
+
text: string;
|
|
681
|
+
}>;
|
|
682
|
+
structuredContent: {
|
|
683
|
+
_meta: {
|
|
684
|
+
handshakeAction: HandshakeAction;
|
|
685
|
+
};
|
|
686
|
+
status: string;
|
|
687
|
+
message: string;
|
|
688
|
+
};
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
export { type AuthRequired, type AuthRequiredMeta, CONTEXT_REQUIREMENTS_KEY, type ContextMiddlewareRequest, type ContextRequirementType, type CreateContextMiddlewareOptions, type EIP712Domain, type EIP712TypeField, type ERC20Context, type ERC20TokenBalance, type HandshakeAction, type HandshakeMeta, type HyperliquidAccountSummary, type HyperliquidContext, type HyperliquidOrder, type HyperliquidPerpPosition, type HyperliquidSpotBalance, META_CONTEXT_REQUIREMENTS_KEY, type PolymarketContext, type PolymarketOrder, type PolymarketPosition, type SignatureRequest, type ToolRequirements, type TransactionProposal, type TransactionProposalMeta, type UserContext, type VerifyRequestOptions, type WalletContext, createAuthRequired, createContextMiddleware, createSignatureRequest, createTransactionProposal, isAuthRequired, isHandshakeAction, isOpenMcpMethod, isProtectedMcpMethod, isSignatureRequest, isTransactionProposal, verifyContextRequest, wrapHandshakeResponse };
|
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
|
/**
|
|
@@ -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
|
}
|
|
@@ -127,6 +129,7 @@ var Tools = class {
|
|
|
127
129
|
var ContextClient = class {
|
|
128
130
|
apiKey;
|
|
129
131
|
baseUrl;
|
|
132
|
+
_closed = false;
|
|
130
133
|
/**
|
|
131
134
|
* Discovery resource for searching tools
|
|
132
135
|
*/
|
|
@@ -151,43 +154,95 @@ var ContextClient = class {
|
|
|
151
154
|
this.discovery = new Discovery(this);
|
|
152
155
|
this.tools = new Tools(this);
|
|
153
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Close the client and clean up resources.
|
|
159
|
+
* After calling close(), any in-flight requests may be aborted.
|
|
160
|
+
*/
|
|
161
|
+
close() {
|
|
162
|
+
this._closed = true;
|
|
163
|
+
}
|
|
154
164
|
/**
|
|
155
165
|
* Internal method for making authenticated HTTP requests
|
|
156
|
-
*
|
|
166
|
+
* Includes timeout (30s) and retry with exponential backoff for transient errors
|
|
157
167
|
*
|
|
158
168
|
* @internal
|
|
159
169
|
*/
|
|
160
|
-
async
|
|
170
|
+
async _fetch(endpoint, options = {}) {
|
|
171
|
+
if (this._closed) {
|
|
172
|
+
throw new ContextError("Client has been closed");
|
|
173
|
+
}
|
|
161
174
|
const url = `${this.baseUrl}${endpoint}`;
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
if (!response.ok) {
|
|
171
|
-
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
172
|
-
let errorCode;
|
|
173
|
-
let helpUrl;
|
|
175
|
+
const maxRetries = 3;
|
|
176
|
+
const timeoutMs = 3e4;
|
|
177
|
+
let lastError;
|
|
178
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
174
181
|
try {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
const response = await fetch(url, {
|
|
183
|
+
...options,
|
|
184
|
+
signal: controller.signal,
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
188
|
+
...options.headers
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
194
|
+
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
199
|
+
let errorCode;
|
|
200
|
+
let helpUrl;
|
|
201
|
+
try {
|
|
202
|
+
const errorBody = await response.json();
|
|
203
|
+
if (errorBody.error) {
|
|
204
|
+
errorMessage = errorBody.error;
|
|
205
|
+
errorCode = errorBody.code;
|
|
206
|
+
helpUrl = errorBody.helpUrl;
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
throw new ContextError(errorMessage, errorCode, response.status, helpUrl);
|
|
211
|
+
}
|
|
212
|
+
return response.json();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
clearTimeout(timeout);
|
|
215
|
+
if (error instanceof ContextError) {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
219
|
+
const isRetryable = lastError.name === "AbortError" || lastError.message.includes("fetch failed") || lastError.message.includes("ECONNRESET") || lastError.message.includes("ETIMEDOUT");
|
|
220
|
+
if (isRetryable && attempt < maxRetries) {
|
|
221
|
+
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
223
|
+
continue;
|
|
180
224
|
}
|
|
181
|
-
|
|
225
|
+
if (lastError.name === "AbortError") {
|
|
226
|
+
throw new ContextError(
|
|
227
|
+
`Request timed out after ${timeoutMs / 1e3}s`,
|
|
228
|
+
void 0,
|
|
229
|
+
408
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
throw new ContextError(
|
|
233
|
+
lastError.message,
|
|
234
|
+
void 0,
|
|
235
|
+
void 0
|
|
236
|
+
);
|
|
182
237
|
}
|
|
183
|
-
throw new ContextError(errorMessage, errorCode, response.status, helpUrl);
|
|
184
238
|
}
|
|
185
|
-
|
|
239
|
+
throw lastError ?? new ContextError("Request failed after retries");
|
|
186
240
|
}
|
|
187
241
|
};
|
|
188
242
|
|
|
189
243
|
// src/context/index.ts
|
|
190
244
|
var CONTEXT_REQUIREMENTS_KEY = "x-context-requirements";
|
|
245
|
+
var META_CONTEXT_REQUIREMENTS_KEY = "contextRequirements";
|
|
191
246
|
var CONTEXT_PLATFORM_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
192
247
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9YOgdpkmVQ5aoNovjsu
|
|
193
248
|
chJdV54OT7dUdbVXz914a7Px8EwnpDqhsvG7WO8xL8sj2Rn6ueAJBk+04Hy/P/UN
|
|
@@ -197,6 +252,41 @@ lfOKbr7w0u72dZjiZPwnNDsX6PEEgvfmoautTFYTQgnZjDzq8UimTcv3KF+hJ5Ep
|
|
|
197
252
|
weipe6amt9lzQzi8WXaFKpOXHQs//WDlUytz/Hl8pvd5craZKzo6Kyrg1Vfan7H3
|
|
198
253
|
TQIDAQAB
|
|
199
254
|
-----END PUBLIC KEY-----`;
|
|
255
|
+
var JWKS_URL = "https://ctxprotocol.com/.well-known/jwks.json";
|
|
256
|
+
var KEY_CACHE_TTL_MS = 36e5;
|
|
257
|
+
var cachedPublicKey = null;
|
|
258
|
+
var cacheTimestamp = 0;
|
|
259
|
+
async function getPlatformPublicKey() {
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
if (cachedPublicKey && now - cacheTimestamp < KEY_CACHE_TTL_MS) {
|
|
262
|
+
return cachedPublicKey;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
267
|
+
const response = await fetch(JWKS_URL, { signal: controller.signal });
|
|
268
|
+
clearTimeout(timeout);
|
|
269
|
+
if (response.ok) {
|
|
270
|
+
const jwks = await response.json();
|
|
271
|
+
if (jwks.keys && jwks.keys.length > 0) {
|
|
272
|
+
const key = jwks.keys[0];
|
|
273
|
+
if (key.x5c && key.x5c.length > 0) {
|
|
274
|
+
const pem = `-----BEGIN CERTIFICATE-----
|
|
275
|
+
${key.x5c[0]}
|
|
276
|
+
-----END CERTIFICATE-----`;
|
|
277
|
+
const { importX509 } = await import('jose');
|
|
278
|
+
cachedPublicKey = await importX509(pem, "RS256");
|
|
279
|
+
cacheTimestamp = now;
|
|
280
|
+
return cachedPublicKey;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
cachedPublicKey = await importSPKI(CONTEXT_PLATFORM_PUBLIC_KEY_PEM, "RS256");
|
|
287
|
+
cacheTimestamp = now;
|
|
288
|
+
return cachedPublicKey;
|
|
289
|
+
}
|
|
200
290
|
var PROTECTED_MCP_METHODS = /* @__PURE__ */ new Set([
|
|
201
291
|
"tools/call"
|
|
202
292
|
// Uncomment these if you want to protect resource/prompt access:
|
|
@@ -228,7 +318,7 @@ async function verifyContextRequest(options) {
|
|
|
228
318
|
}
|
|
229
319
|
const token = authorizationHeader.split(" ")[1];
|
|
230
320
|
try {
|
|
231
|
-
const publicKey = await
|
|
321
|
+
const publicKey = await getPlatformPublicKey();
|
|
232
322
|
const { payload } = await jwtVerify(token, publicKey, {
|
|
233
323
|
issuer: "https://ctxprotocol.com",
|
|
234
324
|
audience
|
|
@@ -262,6 +352,56 @@ function createContextMiddleware(options = {}) {
|
|
|
262
352
|
};
|
|
263
353
|
}
|
|
264
354
|
|
|
265
|
-
|
|
355
|
+
// src/handshake/types.ts
|
|
356
|
+
function isHandshakeAction(value) {
|
|
357
|
+
return typeof value === "object" && value !== null && "_action" in value && (value._action === "signature_request" || value._action === "transaction_proposal" || value._action === "auth_required");
|
|
358
|
+
}
|
|
359
|
+
function isSignatureRequest(value) {
|
|
360
|
+
return isHandshakeAction(value) && value._action === "signature_request";
|
|
361
|
+
}
|
|
362
|
+
function isTransactionProposal(value) {
|
|
363
|
+
return isHandshakeAction(value) && value._action === "transaction_proposal";
|
|
364
|
+
}
|
|
365
|
+
function isAuthRequired(value) {
|
|
366
|
+
return isHandshakeAction(value) && value._action === "auth_required";
|
|
367
|
+
}
|
|
368
|
+
function createSignatureRequest(params) {
|
|
369
|
+
return {
|
|
370
|
+
_action: "signature_request",
|
|
371
|
+
...params
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function createTransactionProposal(params) {
|
|
375
|
+
return {
|
|
376
|
+
_action: "transaction_proposal",
|
|
377
|
+
...params
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function createAuthRequired(params) {
|
|
381
|
+
return {
|
|
382
|
+
_action: "auth_required",
|
|
383
|
+
...params
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function wrapHandshakeResponse(action) {
|
|
387
|
+
const actionType = action._action.replace("_", " ");
|
|
388
|
+
return {
|
|
389
|
+
content: [
|
|
390
|
+
{
|
|
391
|
+
type: "text",
|
|
392
|
+
text: `Handshake required: ${actionType}. Please approve in the Context app.`
|
|
393
|
+
}
|
|
394
|
+
],
|
|
395
|
+
structuredContent: {
|
|
396
|
+
_meta: {
|
|
397
|
+
handshakeAction: action
|
|
398
|
+
},
|
|
399
|
+
status: "handshake_required",
|
|
400
|
+
message: action.meta?.description ?? `${actionType} required`
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export { CONTEXT_REQUIREMENTS_KEY, ContextClient, ContextError, Discovery, META_CONTEXT_REQUIREMENTS_KEY, Tools, createAuthRequired, createContextMiddleware, createSignatureRequest, createTransactionProposal, isAuthRequired, isHandshakeAction, isOpenMcpMethod, isProtectedMcpMethod, isSignatureRequest, isTransactionProposal, verifyContextRequest, wrapHandshakeResponse };
|
|
266
406
|
//# sourceMappingURL=index.js.map
|
|
267
407
|
//# sourceMappingURL=index.js.map
|