@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,7 +2,7 @@ import { Command } from '@oclif/core';
2
2
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
- import * as fs2 from 'fs';
5
+ import * as fs from 'fs';
6
6
  import * as crypto from 'crypto';
7
7
  import * as path from 'path';
8
8
  import * as os from 'os';
@@ -16,10 +16,13 @@ var rpcMethodSchema = {
16
16
  params: z.any().optional().describe(
17
17
  "Method parameters \u2014 structure varies by method. Read the jaw://api-reference/{method} resource for the expected format."
18
18
  ),
19
- chainId: z.number().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia")
19
+ chainId: z.number().optional().describe("Target chain ID (overrides default). E.g., 1 for Ethereum, 8453 for Base, 84532 for Base Sepolia"),
20
+ session: z.boolean().optional().describe(
21
+ "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."
22
+ )
20
23
  };
21
24
  var configSetSchema = {
22
- key: z.enum(["apiKey", "defaultChain", "keysUrl", "ens", "relayUrl"]).describe("Config key"),
25
+ key: z.enum(["apiKey", "defaultChain", "keysUrl", "ens", "relayUrl", "sessionExpiry"]).describe("Config key"),
23
26
  value: z.string().describe("Config value")
24
27
  };
25
28
 
@@ -80,8 +83,8 @@ function isValidRelayUrl(url) {
80
83
 
81
84
  // src/lib/config.ts
82
85
  function ensureDir(dir) {
83
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
84
- fs2.chmodSync(dir, 448);
86
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
87
+ fs.chmodSync(dir, 448);
85
88
  }
86
89
  function migrateConfig(config) {
87
90
  if (config.paymasterUrl && !config.paymasters) {
@@ -93,10 +96,10 @@ function migrateConfig(config) {
93
96
  return config;
94
97
  }
95
98
  function loadConfig() {
96
- if (!fs2.existsSync(PATHS.config)) {
99
+ if (!fs.existsSync(PATHS.config)) {
97
100
  return {};
98
101
  }
99
- const raw = fs2.readFileSync(PATHS.config, "utf-8");
102
+ const raw = fs.readFileSync(PATHS.config, "utf-8");
100
103
  try {
101
104
  const config = JSON.parse(raw);
102
105
  return migrateConfig(config);
@@ -108,15 +111,36 @@ function loadConfig() {
108
111
  }
109
112
  function saveConfig(config) {
110
113
  ensureDir(PATHS.root);
111
- fs2.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
114
+ fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
112
115
  encoding: "utf-8",
113
116
  mode: 384
114
117
  });
115
118
  }
119
+ function redactUrlSecrets(url) {
120
+ try {
121
+ const parsed = new URL(url);
122
+ for (const key of [...parsed.searchParams.keys()]) {
123
+ parsed.searchParams.set(key, "***");
124
+ }
125
+ return parsed.toString();
126
+ } catch {
127
+ return "***";
128
+ }
129
+ }
116
130
  function redactConfig(config) {
117
131
  return {
118
132
  ...config,
119
- apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : void 0
133
+ apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : void 0,
134
+ ...config.paymasters && {
135
+ paymasters: Object.fromEntries(
136
+ Object.entries(config.paymasters).map(([chainId, pm]) => [
137
+ chainId,
138
+ // context is a free-form object (usually just a sponsorshipPolicyId) but a
139
+ // provider could stash a token there, so mask it rather than hand it to the agent.
140
+ { ...pm, url: redactUrlSecrets(pm.url), context: pm.context ? "***" : void 0 }
141
+ ])
142
+ )
143
+ }
120
144
  };
121
145
  }
122
146
  function setConfigValue(key, value) {
@@ -505,14 +529,10 @@ function safeParse(data) {
505
529
  return null;
506
530
  }
507
531
  }
508
-
509
- // src/lib/bridge-singleton.ts
510
- var DEFAULT_KEYS_URL = "https://keys.jaw.id";
511
- var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
512
532
  function loadRelaySession() {
513
533
  try {
514
- if (!fs2.existsSync(PATHS.relay)) return null;
515
- const raw = fs2.readFileSync(PATHS.relay, "utf-8");
534
+ if (!fs.existsSync(PATHS.relay)) return null;
535
+ const raw = fs.readFileSync(PATHS.relay, "utf-8");
516
536
  const parsed = JSON.parse(raw);
517
537
  if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {
518
538
  return null;
@@ -524,17 +544,21 @@ function loadRelaySession() {
524
544
  }
525
545
  function saveRelaySession(info) {
526
546
  ensureDir(PATHS.root);
527
- fs2.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
547
+ fs.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
528
548
  encoding: "utf-8",
529
549
  mode: 384
530
550
  });
531
551
  }
532
552
  function deleteRelaySession() {
533
553
  try {
534
- if (fs2.existsSync(PATHS.relay)) fs2.unlinkSync(PATHS.relay);
554
+ if (fs.existsSync(PATHS.relay)) fs.unlinkSync(PATHS.relay);
535
555
  } catch {
536
556
  }
537
557
  }
558
+
559
+ // src/lib/bridge-singleton.ts
560
+ var DEFAULT_KEYS_URL = "https://keys.jaw.id";
561
+ var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
538
562
  async function getBridge(options) {
539
563
  const config = loadConfig();
540
564
  const keysUrl = options.keysUrl ?? config.keysUrl ?? DEFAULT_KEYS_URL;
@@ -633,8 +657,8 @@ async function shutdownDaemon() {
633
657
  const legacyLog = PATHS.root + "/daemon.log";
634
658
  const legacyLock = PATHS.root + "/daemon.lock";
635
659
  try {
636
- if (fs2.existsSync(legacyBridge)) {
637
- const info = JSON.parse(fs2.readFileSync(legacyBridge, "utf-8"));
660
+ if (fs.existsSync(legacyBridge)) {
661
+ const info = JSON.parse(fs.readFileSync(legacyBridge, "utf-8"));
638
662
  if (info.pid && Number.isInteger(info.pid) && info.pid > 0) {
639
663
  try {
640
664
  process.kill(info.pid, "SIGTERM");
@@ -646,44 +670,213 @@ async function shutdownDaemon() {
646
670
  }
647
671
  for (const f of [legacyBridge, legacyLog, legacyLock]) {
648
672
  try {
649
- if (fs2.existsSync(f)) fs2.unlinkSync(f);
673
+ if (fs.existsSync(f)) fs.unlinkSync(f);
650
674
  } catch {
651
675
  }
652
676
  }
653
677
  }
678
+ function loadSessionKey() {
679
+ if (!fs.existsSync(PATHS.keystore)) {
680
+ throw new Error("No session configured. Run `jaw session setup` first.");
681
+ }
682
+ const contents = fs.readFileSync(PATHS.keystore, "utf-8");
683
+ let parsed;
684
+ try {
685
+ parsed = JSON.parse(contents);
686
+ } catch {
687
+ throw new Error(`Keystore at ${PATHS.keystore} is corrupted. Run \`jaw session setup\` to recreate it.`);
688
+ }
689
+ return parsed.privateKey;
690
+ }
691
+ function keystoreExists() {
692
+ return fs.existsSync(PATHS.keystore);
693
+ }
694
+ function loadSessionConfig() {
695
+ if (!fs.existsSync(PATHS.sessionConfig)) {
696
+ throw new Error("No session configured. Run `jaw session setup` first.");
697
+ }
698
+ const raw = fs.readFileSync(PATHS.sessionConfig, "utf-8");
699
+ try {
700
+ return JSON.parse(raw);
701
+ } catch {
702
+ throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \`jaw session setup\` to recreate it.`);
703
+ }
704
+ }
705
+
706
+ // src/lib/session-bridge.ts
707
+ var SessionBridge = class {
708
+ options;
709
+ session = null;
710
+ constructor(options) {
711
+ this.options = { ...options };
712
+ if (!this.options.paymasterUrl) {
713
+ const config = loadConfig();
714
+ const pm = config.paymasters?.[this.options.chainId];
715
+ if (pm) {
716
+ this.options.paymasterUrl = pm.url;
717
+ this.options.paymasterContext = pm.context;
718
+ }
719
+ }
720
+ }
721
+ async getSession() {
722
+ if (this.session) {
723
+ this.checkExpiry(this.session.config);
724
+ return this.session;
725
+ }
726
+ const config = loadSessionConfig();
727
+ this.checkExpiry(config);
728
+ if (config.chainId !== this.options.chainId) {
729
+ throw new Error(
730
+ `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.`
731
+ );
732
+ }
733
+ let privateKeyHex = loadSessionKey();
734
+ const { privateKeyToAccount } = await import('viem/accounts');
735
+ const localAccount = privateKeyToAccount(privateKeyHex);
736
+ privateKeyHex = null;
737
+ const { Account } = await import('@jaw.id/core');
738
+ const account = await Account.fromLocalAccount(
739
+ {
740
+ chainId: this.options.chainId,
741
+ apiKey: this.options.apiKey,
742
+ paymasterUrl: this.options.paymasterUrl,
743
+ paymasterContext: this.options.paymasterContext
744
+ },
745
+ localAccount
746
+ );
747
+ this.session = { account, config };
748
+ return this.session;
749
+ }
750
+ checkExpiry(config) {
751
+ if (config.expiry <= Date.now() / 1e3) {
752
+ const expiryDate = new Date(config.expiry * 1e3).toISOString();
753
+ throw new Error(`Session expired on ${expiryDate}. Run \`jaw session setup\` to create a new session.`);
754
+ }
755
+ }
756
+ async request(method, params) {
757
+ const { account, config } = await this.getSession();
758
+ switch (method) {
759
+ case "eth_requestAccounts":
760
+ case "eth_accounts":
761
+ return [config.sessionAddress];
762
+ case "wallet_sendCalls": {
763
+ const payload = Array.isArray(params) ? params[0] : params;
764
+ const { calls } = payload;
765
+ return account.sendCalls(calls, {
766
+ permissionId: config.permissionId
767
+ });
768
+ }
769
+ case "wallet_getCallsStatus": {
770
+ const batchId = Array.isArray(params) ? params[0] : params;
771
+ return account.getCallStatus(batchId);
772
+ }
773
+ case "personal_sign": {
774
+ const message = Array.isArray(params) ? params[0] : params;
775
+ return account.signMessage(message);
776
+ }
777
+ case "eth_signTypedData_v4": {
778
+ const asArray = Array.isArray(params) ? params : [params];
779
+ const raw = asArray.length > 1 ? asArray[1] : asArray[0];
780
+ const typedData = typeof raw === "string" ? JSON.parse(raw) : raw;
781
+ return account.signTypedData(typedData);
782
+ }
783
+ case "wallet_grantPermissions":
784
+ throw new Error("Requires browser \u2014 run `jaw session setup`.");
785
+ case "wallet_revokePermissions":
786
+ throw new Error("Requires browser \u2014 run `jaw session revoke`.");
787
+ default:
788
+ throw new Error(`Method ${method} is not supported in auto mode.`);
789
+ }
790
+ }
791
+ close() {
792
+ }
793
+ };
794
+
795
+ // src/lib/rpc-classifier.ts
796
+ var SESSION_SUPPORTED_METHODS = /* @__PURE__ */ new Set([
797
+ "eth_requestAccounts",
798
+ "eth_accounts",
799
+ "wallet_sendCalls",
800
+ "wallet_getCallsStatus",
801
+ "personal_sign",
802
+ "eth_signTypedData_v4"
803
+ ]);
804
+ function supportsSessionMode(method) {
805
+ return SESSION_SUPPORTED_METHODS.has(method);
806
+ }
654
807
 
655
808
  // src/mcp/handlers/rpc.ts
656
- function resolveApiKey() {
657
- const apiKey = process.env["JAW_API_KEY"] ?? loadConfig().apiKey;
809
+ function resolveApiKey(config) {
810
+ const apiKey = process.env["JAW_API_KEY"] ?? config.apiKey;
658
811
  if (!apiKey) {
659
812
  throw new Error("API key required. Set JAW_API_KEY env var or run: jaw config set apiKey <key>");
660
813
  }
661
814
  return apiKey;
662
815
  }
816
+ function resolveChainId(paramChainId, config) {
817
+ if (paramChainId) return paramChainId;
818
+ const envChainId = parseInt(process.env["JAW_CHAIN_ID"] ?? "", 10);
819
+ if (Number.isInteger(envChainId) && envChainId > 0) return envChainId;
820
+ return config.defaultChain ?? 1;
821
+ }
822
+ function envSessionEnabled() {
823
+ const value = process.env["JAW_SESSION"]?.toLowerCase();
824
+ return value === "1" || value === "true";
825
+ }
826
+ var SIGN_RATE_WINDOW_MS = 6e4;
827
+ var MAX_SIGNS_PER_WINDOW = 5;
828
+ var SESSION_SIGNING_METHODS = ["wallet_sendCalls", "personal_sign", "eth_signTypedData_v4"];
663
829
  function registerRpcTool(server) {
664
- server.tool(
830
+ const recentSigns = [];
831
+ function assertUnderSignLimit() {
832
+ const now = Date.now();
833
+ while (recentSigns.length && now - recentSigns[0] > SIGN_RATE_WINDOW_MS) recentSigns.shift();
834
+ if (recentSigns.length >= MAX_SIGNS_PER_WINDOW) {
835
+ throw new Error("Autonomous signing rate limit reached, retry shortly or call again with session: false.");
836
+ }
837
+ recentSigns.push(now);
838
+ }
839
+ server.registerTool(
665
840
  "jaw_rpc",
666
- "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.",
667
- rpcMethodSchema,
841
+ {
842
+ 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.",
843
+ inputSchema: rpcMethodSchema
844
+ },
845
+ // @ts-expect-error — MCP SDK deep type inference with z.any() in schema
668
846
  async (params) => {
669
- const config = loadConfig();
670
- const apiKey = resolveApiKey();
671
- const chainId = params.chainId ?? config.defaultChain ?? 1;
672
- const pm = config.paymasters?.[chainId];
673
- const bridge = await getBridge({
674
- keysUrl: config.keysUrl,
675
- apiKey,
676
- chainId,
677
- ens: config.ens,
678
- paymasterUrl: pm?.url
679
- });
680
847
  try {
681
- const result = await bridge.request(params.method, params.params);
682
- return mcpResult(result);
848
+ const config = loadConfig();
849
+ const apiKey = resolveApiKey(config);
850
+ const chainId = resolveChainId(params.chainId, config);
851
+ const useSession = params.session ?? envSessionEnabled();
852
+ let bridge;
853
+ if (useSession) {
854
+ if (!supportsSessionMode(params.method)) {
855
+ throw new Error(
856
+ `Method ${params.method} is not supported in session mode. Call again with session: false to route through the browser bridge.`
857
+ );
858
+ }
859
+ if (SESSION_SIGNING_METHODS.includes(params.method)) {
860
+ assertUnderSignLimit();
861
+ }
862
+ bridge = new SessionBridge({ apiKey, chainId });
863
+ } else {
864
+ bridge = await getBridge({
865
+ keysUrl: config.keysUrl,
866
+ apiKey,
867
+ chainId,
868
+ ens: config.ens,
869
+ paymasterUrl: config.paymasters?.[chainId]?.url
870
+ });
871
+ }
872
+ try {
873
+ const result = await bridge.request(params.method, params.params);
874
+ return mcpResult(result);
875
+ } finally {
876
+ bridge.close();
877
+ }
683
878
  } catch (err) {
684
879
  return mcpError(err);
685
- } finally {
686
- bridge.close();
687
880
  }
688
881
  }
689
882
  );
@@ -691,26 +884,32 @@ function registerRpcTool(server) {
691
884
 
692
885
  // src/mcp/handlers/config.ts
693
886
  function registerConfigTools(server) {
694
- server.tool("jaw_config_show", "Show current CLI configuration (API key redacted).", {}, async () => {
695
- try {
696
- const config = redactConfig(loadConfig());
697
- return {
698
- content: [{ type: "text", text: JSON.stringify(config) }]
699
- };
700
- } catch (err) {
701
- return mcpError(err);
887
+ server.registerTool(
888
+ "jaw_config_show",
889
+ {
890
+ description: "Show current CLI configuration (secrets redacted).",
891
+ annotations: { readOnlyHint: true }
892
+ },
893
+ async () => {
894
+ try {
895
+ return mcpResult(redactConfig(loadConfig()));
896
+ } catch (err) {
897
+ return mcpError(err);
898
+ }
702
899
  }
703
- });
704
- server.tool(
900
+ );
901
+ server.registerTool(
705
902
  "jaw_config_set",
706
- "Set a CLI configuration value (apiKey, defaultChain, keysUrl, ens, relayUrl).",
707
- configSetSchema,
903
+ {
904
+ description: "Set a CLI configuration value (apiKey, defaultChain, keysUrl, ens, relayUrl, sessionExpiry).",
905
+ inputSchema: configSetSchema
906
+ },
708
907
  async (params) => {
709
908
  try {
710
- if (params.key === "defaultChain") {
909
+ if (params.key === "defaultChain" || params.key === "sessionExpiry") {
711
910
  const num = parseInt(params.value, 10);
712
911
  if (isNaN(num) || num <= 0) {
713
- throw new Error(`Invalid chain ID: ${params.value}`);
912
+ throw new Error(`Invalid number for ${params.key}: ${params.value}`);
714
913
  }
715
914
  setConfigValue(params.key, num);
716
915
  } else {
@@ -730,40 +929,34 @@ function registerConfigTools(server) {
730
929
  }
731
930
  );
732
931
  }
932
+
933
+ // src/mcp/handlers/daemon.ts
733
934
  function registerDaemonTools(server) {
734
- server.tool(
935
+ server.registerTool(
735
936
  "jaw_status",
736
- "Check the current status of the JAW.id relay bridge \u2014 whether a relay session exists, the bridge connection is active, and what configuration is in use.",
737
- {},
937
+ {
938
+ description: "Check the current status of the JAW.id relay bridge \u2014 whether a browser-paired relay session exists (established key exchange) and what configuration is in use. Each jaw_rpc call connects on demand, so there is no persistent connection to report.",
939
+ annotations: { readOnlyHint: true }
940
+ },
738
941
  async () => {
739
942
  try {
740
- let relaySession = false;
741
- try {
742
- if (fs2.existsSync(PATHS.relay)) {
743
- JSON.parse(fs2.readFileSync(PATHS.relay, "utf-8"));
744
- relaySession = true;
745
- }
746
- } catch {
747
- relaySession = false;
748
- }
749
- const config = redactConfig(loadConfig());
943
+ const relaySession = loadRelaySession();
944
+ const established = relaySession !== null && relaySession.peerPublicKey !== null;
750
945
  const status = {
751
- relay: relaySession ? { session: true } : { session: false },
752
- bridgeConnection: "disconnected",
753
- config
754
- };
755
- return {
756
- content: [{ type: "text", text: JSON.stringify(status, null, 2) }]
946
+ relay: established ? { session: true, startedAt: relaySession.startedAt } : { session: false },
947
+ config: redactConfig(loadConfig())
757
948
  };
949
+ return mcpResult(status);
758
950
  } catch (err) {
759
951
  return mcpError(err);
760
952
  }
761
953
  }
762
954
  );
763
- server.tool(
955
+ server.registerTool(
764
956
  "jaw_disconnect",
765
- "Close the relay session and browser tab. Call this when you are done making wallet requests to clean up resources.",
766
- {},
957
+ {
958
+ description: "Close the relay session and browser tab. Call this when you are done making wallet requests to clean up resources."
959
+ },
767
960
  async () => {
768
961
  try {
769
962
  await shutdownDaemon();
@@ -781,9 +974,39 @@ function registerDaemonTools(server) {
781
974
  }
782
975
  );
783
976
  }
977
+
978
+ // src/mcp/handlers/session.ts
979
+ function registerSessionTools(server) {
980
+ server.registerTool(
981
+ "jaw_session_status",
982
+ {
983
+ description: "Show the local session-key (auto mode) status \u2014 session address, owner, permission ID, chain, and expiry. When a valid session exists, jaw_rpc can sign autonomously with session: true instead of opening the browser. Sessions are created with `jaw session setup` in a terminal (requires a one-time browser passkey approval).",
984
+ annotations: { readOnlyHint: true }
985
+ },
986
+ async () => {
987
+ try {
988
+ if (!keystoreExists()) {
989
+ return mcpResult({
990
+ exists: false,
991
+ hint: "No session key. Ask the user to run `jaw session setup` in a terminal to enable autonomous signing."
992
+ });
993
+ }
994
+ const config = loadSessionConfig();
995
+ return mcpResult({
996
+ exists: true,
997
+ ...config,
998
+ expired: config.expiry <= Date.now() / 1e3
999
+ });
1000
+ } catch (err) {
1001
+ return mcpError(err);
1002
+ }
1003
+ }
1004
+ );
1005
+ }
784
1006
  var DOCS_BASE = "https://docs.jaw.id/api-reference";
1007
+ var FETCH_TIMEOUT_MS = 15e3;
785
1008
  async function fetchDocs(url) {
786
- const res = await fetch(url);
1009
+ const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
787
1010
  if (!res.ok) {
788
1011
  throw new Error(`Failed to fetch docs: ${res.status} ${res.statusText}`);
789
1012
  }
@@ -791,7 +1014,7 @@ async function fetchDocs(url) {
791
1014
  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();
792
1015
  }
793
1016
  function registerResources(server) {
794
- server.resource(
1017
+ server.registerResource(
795
1018
  "api-reference",
796
1019
  "jaw://api-reference",
797
1020
  {
@@ -808,7 +1031,7 @@ function registerResources(server) {
808
1031
  ]
809
1032
  })
810
1033
  );
811
- server.resource(
1034
+ server.registerResource(
812
1035
  "api-reference-method",
813
1036
  new ResourceTemplate("jaw://api-reference/{method}", { list: void 0 }),
814
1037
  {
@@ -834,19 +1057,20 @@ function registerResources(server) {
834
1057
  }
835
1058
 
836
1059
  // src/mcp/server.ts
837
- function createMcpServer() {
1060
+ function createMcpServer(version = "0.0.0") {
838
1061
  const server = new McpServer({
839
1062
  name: "jaw",
840
- version: "0.0.1"
1063
+ version
841
1064
  });
842
1065
  registerRpcTool(server);
843
1066
  registerConfigTools(server);
844
1067
  registerDaemonTools(server);
1068
+ registerSessionTools(server);
845
1069
  registerResources(server);
846
1070
  return server;
847
1071
  }
848
- async function startMcpServer() {
849
- const server = createMcpServer();
1072
+ async function startMcpServer(version) {
1073
+ const server = createMcpServer(version);
850
1074
  const transport = new StdioServerTransport();
851
1075
  await server.connect(transport);
852
1076
  }
@@ -856,7 +1080,7 @@ var Mcp = class extends Command {
856
1080
  static description = "Start the MCP server for AI agents (stdio transport)";
857
1081
  static examples = ["<%= config.bin %> mcp"];
858
1082
  async run() {
859
- await startMcpServer();
1083
+ await startMcpServer(this.config.version);
860
1084
  }
861
1085
  };
862
1086