@jaw.id/cli 0.1.16 → 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.
- package/dist/base-command.js.map +1 -1
- package/dist/commands/config/set.js.map +1 -1
- package/dist/commands/config/show.js +22 -1
- package/dist/commands/config/show.js.map +1 -1
- package/dist/commands/config/write.js.map +1 -1
- package/dist/commands/disconnect.js +14 -14
- package/dist/commands/disconnect.js.map +1 -1
- package/dist/commands/mcp/index.js +308 -84
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/rpc/call.js +4 -4
- package/dist/commands/rpc/call.js.map +1 -1
- package/dist/commands/session/revoke.js +4 -4
- package/dist/commands/session/revoke.js.map +1 -1
- package/dist/commands/session/setup.js +4 -4
- package/dist/commands/session/setup.js.map +1 -1
- package/dist/commands/session/status.js.map +1 -1
- package/dist/commands/version.js.map +1 -1
- package/dist/index.js +17 -15
- package/dist/index.js.map +1 -1
- package/dist/lib/bridge-singleton.js +17 -17
- package/dist/lib/bridge-singleton.js.map +1 -1
- package/dist/lib/config.js +22 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/keystore.js.map +1 -1
- package/dist/lib/session-bridge.js.map +1 -1
- package/dist/lib/session-config.js.map +1 -1
- package/dist/mcp/handlers/config.js +57 -17
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +60 -39
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/resources.js +4 -3
- package/dist/mcp/handlers/resources.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +195 -26
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/server.js +307 -83
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +5 -2
- package/dist/mcp/tools.js.map +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +4 -1
|
@@ -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
|
|
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
|
-
|
|
84
|
-
|
|
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 (!
|
|
99
|
+
if (!fs.existsSync(PATHS.config)) {
|
|
97
100
|
return {};
|
|
98
101
|
}
|
|
99
|
-
const raw =
|
|
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
|
-
|
|
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 (!
|
|
515
|
-
const raw =
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
637
|
-
const info = JSON.parse(
|
|
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 (
|
|
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"] ??
|
|
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
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
|
682
|
-
|
|
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.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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.
|
|
900
|
+
);
|
|
901
|
+
server.registerTool(
|
|
705
902
|
"jaw_config_set",
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
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.
|
|
935
|
+
server.registerTool(
|
|
735
936
|
"jaw_status",
|
|
736
|
-
|
|
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
|
-
|
|
741
|
-
|
|
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:
|
|
752
|
-
|
|
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.
|
|
955
|
+
server.registerTool(
|
|
764
956
|
"jaw_disconnect",
|
|
765
|
-
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s{2,}/g, " ").trim();
|
|
792
1015
|
}
|
|
793
1016
|
function registerResources(server) {
|
|
794
|
-
server.
|
|
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.
|
|
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
|
|
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
|
|