@jaw.id/cli 0.1.16 → 0.1.18

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