@jaw.id/cli 0.1.15 → 0.1.17

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.
Files changed (40) hide show
  1. package/dist/base-command.js.map +1 -1
  2. package/dist/commands/config/set.js.map +1 -1
  3. package/dist/commands/config/show.js +22 -1
  4. package/dist/commands/config/show.js.map +1 -1
  5. package/dist/commands/config/write.js.map +1 -1
  6. package/dist/commands/disconnect.js +14 -14
  7. package/dist/commands/disconnect.js.map +1 -1
  8. package/dist/commands/mcp/index.js +308 -84
  9. package/dist/commands/mcp/index.js.map +1 -1
  10. package/dist/commands/rpc/call.js +4 -4
  11. package/dist/commands/rpc/call.js.map +1 -1
  12. package/dist/commands/session/revoke.js +4 -4
  13. package/dist/commands/session/revoke.js.map +1 -1
  14. package/dist/commands/session/setup.js +4 -4
  15. package/dist/commands/session/setup.js.map +1 -1
  16. package/dist/commands/session/status.js.map +1 -1
  17. package/dist/commands/version.js.map +1 -1
  18. package/dist/index.js +17 -15
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/bridge-singleton.js +17 -17
  21. package/dist/lib/bridge-singleton.js.map +1 -1
  22. package/dist/lib/config.js +22 -1
  23. package/dist/lib/config.js.map +1 -1
  24. package/dist/lib/keystore.js.map +1 -1
  25. package/dist/lib/session-bridge.js.map +1 -1
  26. package/dist/lib/session-config.js.map +1 -1
  27. package/dist/mcp/handlers/config.js +57 -17
  28. package/dist/mcp/handlers/config.js.map +1 -1
  29. package/dist/mcp/handlers/daemon.js +60 -39
  30. package/dist/mcp/handlers/daemon.js.map +1 -1
  31. package/dist/mcp/handlers/resources.js +4 -3
  32. package/dist/mcp/handlers/resources.js.map +1 -1
  33. package/dist/mcp/handlers/rpc.js +195 -26
  34. package/dist/mcp/handlers/rpc.js.map +1 -1
  35. package/dist/mcp/server.js +307 -83
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/mcp/tools.js +5 -2
  38. package/dist/mcp/tools.js.map +1 -1
  39. package/oclif.manifest.json +1 -1
  40. package/package.json +5 -2
@@ -2,8 +2,9 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
 
3
3
  // src/mcp/handlers/resources.ts
4
4
  var DOCS_BASE = "https://docs.jaw.id/api-reference";
5
+ var FETCH_TIMEOUT_MS = 15e3;
5
6
  async function fetchDocs(url) {
6
- const res = await fetch(url);
7
+ const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
7
8
  if (!res.ok) {
8
9
  throw new Error(`Failed to fetch docs: ${res.status} ${res.statusText}`);
9
10
  }
@@ -11,7 +12,7 @@ async function fetchDocs(url) {
11
12
  return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s{2,}/g, " ").trim();
12
13
  }
13
14
  function registerResources(server) {
14
- server.resource(
15
+ server.registerResource(
15
16
  "api-reference",
16
17
  "jaw://api-reference",
17
18
  {
@@ -28,7 +29,7 @@ function registerResources(server) {
28
29
  ]
29
30
  })
30
31
  );
31
- server.resource(
32
+ server.registerResource(
32
33
  "api-reference-method",
33
34
  new ResourceTemplate("jaw://api-reference/{method}", { list: void 0 }),
34
35
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/mcp/handlers/resources.ts"],"names":[],"mappings":";;;AAEA,IAAM,SAAA,GAAY,mCAAA;AAElB,eAAe,UAAU,GAAA,EAA8B;AACrD,EAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAG,CAAA;AAC3B,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,MAAM,IAAI,MAAM,CAAA,sBAAA,EAAyB,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,CAAE,CAAA;AAAA,EACzE;AACA,EAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAG5B,EAAA,OAAO,KACJ,OAAA,CAAQ,mCAAA,EAAqC,EAAE,CAAA,CAC/C,OAAA,CAAQ,mCAAmC,EAAE,CAAA,CAC7C,OAAA,CAAQ,UAAA,EAAY,GAAG,CAAA,CACvB,OAAA,CAAQ,WAAW,GAAG,CAAA,CACtB,QAAQ,QAAA,EAAU,GAAG,CAAA,CACrB,OAAA,CAAQ,SAAS,GAAG,CAAA,CACpB,QAAQ,OAAA,EAAS,GAAG,EACpB,OAAA,CAAQ,SAAA,EAAW,GAAG,CAAA,CACtB,OAAA,CAAQ,UAAU,GAAG,CAAA,CACrB,QAAQ,SAAA,EAAW,GAAG,EACtB,IAAA,EAAK;AACV;AAEO,SAAS,kBAAkB,MAAA,EAAyB;AAEzD,EAAA,MAAA,CAAO,QAAA;AAAA,IACL,eAAA;AAAA,IACA,qBAAA;AAAA,IACA;AAAA,MACE,WAAA,EACE,yJAAA;AAAA,MAEF,QAAA,EAAU;AAAA,KACZ;AAAA,IACA,aAAa;AAAA,MACX,QAAA,EAAU;AAAA,QACR;AAAA,UACE,GAAA,EAAK,qBAAA;AAAA,UACL,QAAA,EAAU,YAAA;AAAA,UACV,IAAA,EAAM,MAAM,SAAA,CAAU,SAAS;AAAA;AACjC;AACF,KACF;AAAA,GACF;AAGA,EAAA,MAAA,CAAO,QAAA;AAAA,IACL,sBAAA;AAAA,IACA,IAAI,gBAAA,CAAiB,8BAAA,EAAgC,EAAE,IAAA,EAAM,QAAW,CAAA;AAAA,IACxE;AAAA,MACE,WAAA,EACE,2MAAA;AAAA,MAGF,QAAA,EAAU;AAAA,KACZ;AAAA,IACA,OAAO,KAAK,SAAA,KAAc;AACxB,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,SAAA,CAAU,MAAM,CAAA;AAGtC,MAAA,IAAI,CAAC,UAAA,CAAW,IAAA,CAAK,MAAM,CAAA,EAAG;AAC5B,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,MAAM,CAAA,gDAAA,CAAkD,CAAA;AAAA,MACnG;AAEA,MAAA,OAAO;AAAA,QACL,QAAA,EAAU;AAAA,UACR;AAAA,YACE,KAAK,GAAA,CAAI,IAAA;AAAA,YACT,QAAA,EAAU,YAAA;AAAA,YACV,MAAM,MAAM,SAAA,CAAU,GAAG,SAAS,CAAA,CAAA,EAAI,MAAM,CAAA,CAAE;AAAA;AAChD;AACF,OACF;AAAA,IACF;AAAA,GACF;AACF","file":"resources.js","sourcesContent":["import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\n\nconst DOCS_BASE = 'https://docs.jaw.id/api-reference';\n\nasync function fetchDocs(url: string): Promise<string> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`Failed to fetch docs: ${res.status} ${res.statusText}`);\n }\n const html = await res.text();\n // Strip HTML tags for a readable text representation.\n // MCP resources are consumed by LLMs, so plain text is ideal.\n return html\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nexport function registerResources(server: McpServer): void {\n // Overview of all RPC methods\n server.resource(\n 'api-reference',\n 'jaw://api-reference',\n {\n description:\n 'JAW.id API reference — lists all supported RPC methods with descriptions. ' +\n 'Read this before using the jaw_rpc tool to understand available methods.',\n mimeType: 'text/plain',\n },\n async () => ({\n contents: [\n {\n uri: 'jaw://api-reference',\n mimeType: 'text/plain',\n text: await fetchDocs(DOCS_BASE),\n },\n ],\n })\n );\n\n // Per-method documentation with parameter details\n server.resource(\n 'api-reference-method',\n new ResourceTemplate('jaw://api-reference/{method}', { list: undefined }),\n {\n description:\n 'Detailed documentation for a specific RPC method including parameters, ' +\n 'request/response format, and examples. Use the method name from the ' +\n 'api-reference overview (e.g. wallet_sendCalls, personal_sign).',\n mimeType: 'text/plain',\n },\n async (uri, variables) => {\n const method = String(variables.method);\n\n // Validate method name to prevent path traversal (e.g. ../../admin)\n if (!/^[\\w_]+$/.test(method)) {\n throw new Error(`Invalid method name: \"${method}\". Expected an RPC method like wallet_sendCalls.`);\n }\n\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'text/plain',\n text: await fetchDocs(`${DOCS_BASE}/${method}`),\n },\n ],\n };\n }\n );\n}\n"]}
1
+ {"version":3,"sources":["../../../src/mcp/handlers/resources.ts"],"names":[],"mappings":";;;AAEA,IAAM,SAAA,GAAY,mCAAA;AAElB,IAAM,gBAAA,GAAmB,IAAA;AAEzB,eAAe,UAAU,GAAA,EAA8B;AACrD,EAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK,EAAE,QAAQ,WAAA,CAAY,OAAA,CAAQ,gBAAgB,CAAA,EAAG,CAAA;AAC9E,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,MAAM,IAAI,MAAM,CAAA,sBAAA,EAAyB,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,CAAE,CAAA;AAAA,EACzE;AACA,EAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAG5B,EAAA,OAAO,KACJ,OAAA,CAAQ,mCAAA,EAAqC,EAAE,CAAA,CAC/C,OAAA,CAAQ,mCAAmC,EAAE,CAAA,CAC7C,OAAA,CAAQ,UAAA,EAAY,GAAG,CAAA,CACvB,OAAA,CAAQ,WAAW,GAAG,CAAA,CACtB,QAAQ,QAAA,EAAU,GAAG,CAAA,CACrB,OAAA,CAAQ,SAAS,GAAG,CAAA,CACpB,QAAQ,OAAA,EAAS,GAAG,EACpB,OAAA,CAAQ,SAAA,EAAW,GAAG,CAAA,CACtB,OAAA,CAAQ,UAAU,GAAG,CAAA,CACrB,QAAQ,SAAA,EAAW,GAAG,EACtB,IAAA,EAAK;AACV;AAEO,SAAS,kBAAkB,MAAA,EAAyB;AAEzD,EAAA,MAAA,CAAO,gBAAA;AAAA,IACL,eAAA;AAAA,IACA,qBAAA;AAAA,IACA;AAAA,MACE,WAAA,EACE,yJAAA;AAAA,MAEF,QAAA,EAAU;AAAA,KACZ;AAAA,IACA,aAAa;AAAA,MACX,QAAA,EAAU;AAAA,QACR;AAAA,UACE,GAAA,EAAK,qBAAA;AAAA,UACL,QAAA,EAAU,YAAA;AAAA,UACV,IAAA,EAAM,MAAM,SAAA,CAAU,SAAS;AAAA;AACjC;AACF,KACF;AAAA,GACF;AAGA,EAAA,MAAA,CAAO,gBAAA;AAAA,IACL,sBAAA;AAAA,IACA,IAAI,gBAAA,CAAiB,8BAAA,EAAgC,EAAE,IAAA,EAAM,QAAW,CAAA;AAAA,IACxE;AAAA,MACE,WAAA,EACE,2MAAA;AAAA,MAGF,QAAA,EAAU;AAAA,KACZ;AAAA,IACA,OAAO,KAAK,SAAA,KAAc;AACxB,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,SAAA,CAAU,MAAM,CAAA;AAGtC,MAAA,IAAI,CAAC,UAAA,CAAW,IAAA,CAAK,MAAM,CAAA,EAAG;AAC5B,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,MAAM,CAAA,gDAAA,CAAkD,CAAA;AAAA,MACnG;AAEA,MAAA,OAAO;AAAA,QACL,QAAA,EAAU;AAAA,UACR;AAAA,YACE,KAAK,GAAA,CAAI,IAAA;AAAA,YACT,QAAA,EAAU,YAAA;AAAA,YACV,MAAM,MAAM,SAAA,CAAU,GAAG,SAAS,CAAA,CAAA,EAAI,MAAM,CAAA,CAAE;AAAA;AAChD;AACF,OACF;AAAA,IACF;AAAA,GACF;AACF","file":"resources.js","sourcesContent":["import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';\n\nconst DOCS_BASE = 'https://docs.jaw.id/api-reference';\n\nconst FETCH_TIMEOUT_MS = 15_000;\n\nasync function fetchDocs(url: string): Promise<string> {\n const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });\n if (!res.ok) {\n throw new Error(`Failed to fetch docs: ${res.status} ${res.statusText}`);\n }\n const html = await res.text();\n // Strip HTML tags for a readable text representation.\n // MCP resources are consumed by LLMs, so plain text is ideal.\n return html\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\nexport function registerResources(server: McpServer): void {\n // Overview of all RPC methods\n server.registerResource(\n 'api-reference',\n 'jaw://api-reference',\n {\n description:\n 'JAW.id API reference — lists all supported RPC methods with descriptions. ' +\n 'Read this before using the jaw_rpc tool to understand available methods.',\n mimeType: 'text/plain',\n },\n async () => ({\n contents: [\n {\n uri: 'jaw://api-reference',\n mimeType: 'text/plain',\n text: await fetchDocs(DOCS_BASE),\n },\n ],\n })\n );\n\n // Per-method documentation with parameter details\n server.registerResource(\n 'api-reference-method',\n new ResourceTemplate('jaw://api-reference/{method}', { list: undefined }),\n {\n description:\n 'Detailed documentation for a specific RPC method including parameters, ' +\n 'request/response format, and examples. Use the method name from the ' +\n 'api-reference overview (e.g. wallet_sendCalls, personal_sign).',\n mimeType: 'text/plain',\n },\n async (uri, variables) => {\n const method = String(variables.method);\n\n // Validate method name to prevent path traversal (e.g. ../../admin)\n if (!/^[\\w_]+$/.test(method)) {\n throw new Error(`Invalid method name: \"${method}\". Expected an RPC method like wallet_sendCalls.`);\n }\n\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'text/plain',\n text: await fetchDocs(`${DOCS_BASE}/${method}`),\n },\n ],\n };\n }\n );\n}\n"]}
@@ -13,10 +13,13 @@ var rpcMethodSchema = {
13
13
  params: z.any().optional().describe(
14
14
  "Method parameters \u2014 structure varies by method. Read the jaw://api-reference/{method} resource for the expected format."
15
15
  ),
16
- chainId: z.number().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia")
16
+ chainId: z.number().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia"),
17
+ session: z.boolean().optional().describe(
18
+ "Sign with the local session key instead of opening the browser (requires `jaw session setup`; check jaw_session_status first). Supported methods only: eth_requestAccounts, eth_accounts, wallet_sendCalls, wallet_getCallsStatus, personal_sign, eth_signTypedData_v4. Defaults to the JAW_SESSION env var."
19
+ )
17
20
  };
18
21
  ({
19
- key: z.enum(["apiKey", "defaultChain", "keysUrl", "ens", "relayUrl"]).describe("Config key"),
22
+ key: z.enum(["apiKey", "defaultChain", "keysUrl", "ens", "relayUrl", "sessionExpiry"]).describe("Config key"),
20
23
  value: z.string().describe("Config value")
21
24
  });
22
25
 
@@ -485,10 +488,6 @@ function safeParse(data) {
485
488
  return null;
486
489
  }
487
490
  }
488
-
489
- // src/lib/bridge-singleton.ts
490
- var DEFAULT_KEYS_URL = "https://keys.jaw.id";
491
- var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
492
491
  function loadRelaySession() {
493
492
  try {
494
493
  if (!fs.existsSync(PATHS.relay)) return null;
@@ -515,6 +514,10 @@ function deleteRelaySession() {
515
514
  } catch {
516
515
  }
517
516
  }
517
+
518
+ // src/lib/bridge-singleton.ts
519
+ var DEFAULT_KEYS_URL = "https://keys.jaw.id";
520
+ var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
518
521
  async function getBridge(options) {
519
522
  const config = loadConfig();
520
523
  const keysUrl = options.keysUrl ?? config.keysUrl ?? DEFAULT_KEYS_URL;
@@ -592,39 +595,205 @@ function buildBridgeUrl(keysUrl, session, relayUrl, cliPublicKeyHex) {
592
595
  url.hash = `pk=${cliPublicKeyHex}`;
593
596
  return url.toString();
594
597
  }
598
+ function loadSessionKey() {
599
+ if (!fs.existsSync(PATHS.keystore)) {
600
+ throw new Error("No session configured. Run `jaw session setup` first.");
601
+ }
602
+ const contents = fs.readFileSync(PATHS.keystore, "utf-8");
603
+ let parsed;
604
+ try {
605
+ parsed = JSON.parse(contents);
606
+ } catch {
607
+ throw new Error(`Keystore at ${PATHS.keystore} is corrupted. Run \`jaw session setup\` to recreate it.`);
608
+ }
609
+ return parsed.privateKey;
610
+ }
611
+ function loadSessionConfig() {
612
+ if (!fs.existsSync(PATHS.sessionConfig)) {
613
+ throw new Error("No session configured. Run `jaw session setup` first.");
614
+ }
615
+ const raw = fs.readFileSync(PATHS.sessionConfig, "utf-8");
616
+ try {
617
+ return JSON.parse(raw);
618
+ } catch {
619
+ throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \`jaw session setup\` to recreate it.`);
620
+ }
621
+ }
622
+
623
+ // src/lib/session-bridge.ts
624
+ var SessionBridge = class {
625
+ options;
626
+ session = null;
627
+ constructor(options) {
628
+ this.options = { ...options };
629
+ if (!this.options.paymasterUrl) {
630
+ const config = loadConfig();
631
+ const pm = config.paymasters?.[this.options.chainId];
632
+ if (pm) {
633
+ this.options.paymasterUrl = pm.url;
634
+ this.options.paymasterContext = pm.context;
635
+ }
636
+ }
637
+ }
638
+ async getSession() {
639
+ if (this.session) {
640
+ this.checkExpiry(this.session.config);
641
+ return this.session;
642
+ }
643
+ const config = loadSessionConfig();
644
+ this.checkExpiry(config);
645
+ if (config.chainId !== this.options.chainId) {
646
+ throw new Error(
647
+ `Session was created for chain ${config.chainId}, but --chain ${this.options.chainId} was requested. Run \`jaw session setup --chain ${this.options.chainId}\` to create a session for that chain.`
648
+ );
649
+ }
650
+ let privateKeyHex = loadSessionKey();
651
+ const { privateKeyToAccount } = await import('viem/accounts');
652
+ const localAccount = privateKeyToAccount(privateKeyHex);
653
+ privateKeyHex = null;
654
+ const { Account } = await import('@jaw.id/core');
655
+ const account = await Account.fromLocalAccount(
656
+ {
657
+ chainId: this.options.chainId,
658
+ apiKey: this.options.apiKey,
659
+ paymasterUrl: this.options.paymasterUrl,
660
+ paymasterContext: this.options.paymasterContext
661
+ },
662
+ localAccount
663
+ );
664
+ this.session = { account, config };
665
+ return this.session;
666
+ }
667
+ checkExpiry(config) {
668
+ if (config.expiry <= Date.now() / 1e3) {
669
+ const expiryDate = new Date(config.expiry * 1e3).toISOString();
670
+ throw new Error(`Session expired on ${expiryDate}. Run \`jaw session setup\` to create a new session.`);
671
+ }
672
+ }
673
+ async request(method, params) {
674
+ const { account, config } = await this.getSession();
675
+ switch (method) {
676
+ case "eth_requestAccounts":
677
+ case "eth_accounts":
678
+ return [config.sessionAddress];
679
+ case "wallet_sendCalls": {
680
+ const payload = Array.isArray(params) ? params[0] : params;
681
+ const { calls } = payload;
682
+ return account.sendCalls(calls, {
683
+ permissionId: config.permissionId
684
+ });
685
+ }
686
+ case "wallet_getCallsStatus": {
687
+ const batchId = Array.isArray(params) ? params[0] : params;
688
+ return account.getCallStatus(batchId);
689
+ }
690
+ case "personal_sign": {
691
+ const message = Array.isArray(params) ? params[0] : params;
692
+ return account.signMessage(message);
693
+ }
694
+ case "eth_signTypedData_v4": {
695
+ const asArray = Array.isArray(params) ? params : [params];
696
+ const raw = asArray.length > 1 ? asArray[1] : asArray[0];
697
+ const typedData = typeof raw === "string" ? JSON.parse(raw) : raw;
698
+ return account.signTypedData(typedData);
699
+ }
700
+ case "wallet_grantPermissions":
701
+ throw new Error("Requires browser \u2014 run `jaw session setup`.");
702
+ case "wallet_revokePermissions":
703
+ throw new Error("Requires browser \u2014 run `jaw session revoke`.");
704
+ default:
705
+ throw new Error(`Method ${method} is not supported in auto mode.`);
706
+ }
707
+ }
708
+ close() {
709
+ }
710
+ };
711
+
712
+ // src/lib/rpc-classifier.ts
713
+ var SESSION_SUPPORTED_METHODS = /* @__PURE__ */ new Set([
714
+ "eth_requestAccounts",
715
+ "eth_accounts",
716
+ "wallet_sendCalls",
717
+ "wallet_getCallsStatus",
718
+ "personal_sign",
719
+ "eth_signTypedData_v4"
720
+ ]);
721
+ function supportsSessionMode(method) {
722
+ return SESSION_SUPPORTED_METHODS.has(method);
723
+ }
595
724
 
596
725
  // src/mcp/handlers/rpc.ts
597
- function resolveApiKey() {
598
- const apiKey = process.env["JAW_API_KEY"] ?? loadConfig().apiKey;
726
+ function resolveApiKey(config) {
727
+ const apiKey = process.env["JAW_API_KEY"] ?? config.apiKey;
599
728
  if (!apiKey) {
600
729
  throw new Error("API key required. Set JAW_API_KEY env var or run: jaw config set apiKey <key>");
601
730
  }
602
731
  return apiKey;
603
732
  }
733
+ function resolveChainId(paramChainId, config) {
734
+ if (paramChainId) return paramChainId;
735
+ const envChainId = parseInt(process.env["JAW_CHAIN_ID"] ?? "", 10);
736
+ if (Number.isInteger(envChainId) && envChainId > 0) return envChainId;
737
+ return config.defaultChain ?? 1;
738
+ }
739
+ function envSessionEnabled() {
740
+ const value = process.env["JAW_SESSION"]?.toLowerCase();
741
+ return value === "1" || value === "true";
742
+ }
743
+ var SIGN_RATE_WINDOW_MS = 6e4;
744
+ var MAX_SIGNS_PER_WINDOW = 5;
745
+ var SESSION_SIGNING_METHODS = ["wallet_sendCalls", "personal_sign", "eth_signTypedData_v4"];
604
746
  function registerRpcTool(server) {
605
- server.tool(
747
+ const recentSigns = [];
748
+ function assertUnderSignLimit() {
749
+ const now = Date.now();
750
+ while (recentSigns.length && now - recentSigns[0] > SIGN_RATE_WINDOW_MS) recentSigns.shift();
751
+ if (recentSigns.length >= MAX_SIGNS_PER_WINDOW) {
752
+ throw new Error("Autonomous signing rate limit reached, retry shortly or call again with session: false.");
753
+ }
754
+ recentSigns.push(now);
755
+ }
756
+ server.registerTool(
606
757
  "jaw_rpc",
607
- "Execute any JAW.id wallet RPC method via the browser bridge. Supports transactions, signing, permissions, and queries. Methods that require signing will open the browser for passkey authentication. IMPORTANT: Read the jaw://api-reference resource for the full list of methods, and jaw://api-reference/{method} for detailed parameter formats and examples.",
608
- rpcMethodSchema,
758
+ {
759
+ description: "Execute any JAW.id wallet RPC method. Supports transactions, signing, permissions, and queries. By default, methods that require signing open the browser for passkey authentication. Pass session: true to sign autonomously with the local session key instead (requires a session created via `jaw session setup` \u2014 check jaw_session_status). IMPORTANT: Read the jaw://api-reference resource for the full list of methods, and jaw://api-reference/{method} for detailed parameter formats and examples.",
760
+ inputSchema: rpcMethodSchema
761
+ },
762
+ // @ts-expect-error — MCP SDK deep type inference with z.any() in schema
609
763
  async (params) => {
610
- const config = loadConfig();
611
- const apiKey = resolveApiKey();
612
- const chainId = params.chainId ?? config.defaultChain ?? 1;
613
- const pm = config.paymasters?.[chainId];
614
- const bridge = await getBridge({
615
- keysUrl: config.keysUrl,
616
- apiKey,
617
- chainId,
618
- ens: config.ens,
619
- paymasterUrl: pm?.url
620
- });
621
764
  try {
622
- const result = await bridge.request(params.method, params.params);
623
- return mcpResult(result);
765
+ const config = loadConfig();
766
+ const apiKey = resolveApiKey(config);
767
+ const chainId = resolveChainId(params.chainId, config);
768
+ const useSession = params.session ?? envSessionEnabled();
769
+ let bridge;
770
+ if (useSession) {
771
+ if (!supportsSessionMode(params.method)) {
772
+ throw new Error(
773
+ `Method ${params.method} is not supported in session mode. Call again with session: false to route through the browser bridge.`
774
+ );
775
+ }
776
+ if (SESSION_SIGNING_METHODS.includes(params.method)) {
777
+ assertUnderSignLimit();
778
+ }
779
+ bridge = new SessionBridge({ apiKey, chainId });
780
+ } else {
781
+ bridge = await getBridge({
782
+ keysUrl: config.keysUrl,
783
+ apiKey,
784
+ chainId,
785
+ ens: config.ens,
786
+ paymasterUrl: config.paymasters?.[chainId]?.url
787
+ });
788
+ }
789
+ try {
790
+ const result = await bridge.request(params.method, params.params);
791
+ return mcpResult(result);
792
+ } finally {
793
+ bridge.close();
794
+ }
624
795
  } catch (err) {
625
796
  return mcpError(err);
626
- } finally {
627
- bridge.close();
628
797
  }
629
798
  }
630
799
  );