@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
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
83
|
-
|
|
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 (!
|
|
98
|
+
if (!fs.existsSync(PATHS.config)) {
|
|
96
99
|
return {};
|
|
97
100
|
}
|
|
98
|
-
const raw =
|
|
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
|
-
|
|
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 (!
|
|
514
|
-
const raw =
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
636
|
-
const info = JSON.parse(
|
|
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 (
|
|
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"] ??
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
|
|
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
|
|
681
|
-
|
|
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.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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.
|
|
899
|
+
);
|
|
900
|
+
server.registerTool(
|
|
704
901
|
"jaw_config_set",
|
|
705
|
-
|
|
706
|
-
|
|
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
|
|
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.
|
|
934
|
+
server.registerTool(
|
|
734
935
|
"jaw_status",
|
|
735
|
-
|
|
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
|
-
|
|
740
|
-
|
|
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:
|
|
751
|
-
|
|
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.
|
|
954
|
+
server.registerTool(
|
|
763
955
|
"jaw_disconnect",
|
|
764
|
-
|
|
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(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s{2,}/g, " ").trim();
|
|
791
1014
|
}
|
|
792
1015
|
function registerResources(server) {
|
|
793
|
-
server.
|
|
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.
|
|
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
|
|
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
|
}
|