@vultisig/mcp 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @vultisig/mcp
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`0388700`](https://github.com/vultisig/vultisig-sdk/commit/03887009b7579fc0b193d068d4a205cdd3b7c214), [`83fe4c3`](https://github.com/vultisig/vultisig-sdk/commit/83fe4c3c58637aea4823d0eaa7f21d4c5cdf3dc7)]:
8
+ - @vultisig/client-shared@0.2.0
9
+ - @vultisig/sdk@0.16.0
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @vultisig/mcp
2
+
3
+ Model Context Protocol server that exposes the Vultisig SDK to LLM tool-calling hosts (Claude Desktop, IDEs, agent frameworks). Runs over stdio as a JSON-RPC 2.0 transport.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @vultisig/mcp
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Set up credentials via the CLI first
15
+ vsig auth setup
16
+
17
+ # Then run the MCP server (stdio)
18
+ vmcp --vault <id-or-path>
19
+
20
+ # Read-only profile (disables send/swap)
21
+ vmcp --vault <id-or-path> --profile harness
22
+ ```
23
+
24
+ ## Tools
25
+
26
+ | Profile | Tools |
27
+ | --------- | ----------------------------------------------------------------------------------- |
28
+ | `defi` (default) | `get_balances`, `get_portfolio`, `get_address`, `vault_info`, `supported_chains`, `swap_quote`, `send`, `swap` |
29
+ | `harness` | `get_balances`, `get_portfolio`, `get_address`, `vault_info`, `supported_chains`, `swap_quote` (read-only) |
30
+
31
+ ## Confirmation model (important)
32
+
33
+ **Mutating tools (`send`, `swap`) rely on the MCP host to gate human confirmation.** The server treats a call as a broadcast-for-real only when the host passes `confirmed: true` (strict boolean via Zod — `"true"`, `1`, or truthy strings are rejected). Without `confirmed`, the call is a dry-run preview.
34
+
35
+ **This means:**
36
+
37
+ - A well-behaved MCP host (Claude Desktop, Cursor, etc.) **must** show the user the tool-call payload and require explicit confirmation before passing `confirmed: true`.
38
+ - A buggy or malicious host can bypass that gate. **Do not run `@vultisig/mcp` against untrusted hosts when the `defi` profile is active.**
39
+ - For server-deployed MCP instances, prefer `--profile harness` (read-only) unless you have a second-factor gate out of band.
40
+
41
+ If you want to add your own confirmation flow (e.g. 2FA, webhook, manual approval service), run the server with `--profile harness` and wire `send`/`swap` through your own tooling.
42
+
43
+ ## Security notes
44
+
45
+ - Credentials are read from the same keyring (or encrypted file fallback) that the CLI uses — `@napi-rs/keyring` with AES-256-GCM + async scrypt fallback for headless environments.
46
+ - stdio framing is kept strict: all SDK log output is redirected to stderr before initialization to prevent JSON-RPC stream corruption.
47
+ - Tools do not accept arbitrary user-supplied RPC overrides; use CLI config for endpoint customization.
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/mcp-server.ts
4
+ import { readFileSync } from "node:fs";
5
+ import { executeAuthSetup as executeAuthSetup2, getServerPassword as getServerPassword2 } from "@vultisig/client-shared";
6
+ import { Vultisig } from "@vultisig/sdk";
7
+
8
+ // src/index.ts
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+
12
+ // src/tools.ts
13
+ import { descriptions } from "@vultisig/client-shared";
14
+ import * as z from "zod/v4";
15
+ function success(data) {
16
+ return {
17
+ content: [{ type: "text", text: JSON.stringify(data, (_k, v) => typeof v === "bigint" ? v.toString() : v) }]
18
+ };
19
+ }
20
+ function error(message, hint) {
21
+ const payload = { error: message };
22
+ if (hint) payload.hint = hint;
23
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: true };
24
+ }
25
+ async function wrapHandler(fn) {
26
+ try {
27
+ return success(await fn());
28
+ } catch (err) {
29
+ if (err instanceof Error) return error(err.message);
30
+ return error(String(err));
31
+ }
32
+ }
33
+ function resolveChain(name, knownChains) {
34
+ const match = knownChains.find((c) => c.toLowerCase() === name.toLowerCase());
35
+ if (!match) throw new Error(`Unknown chain "${name}". Use supported_chains or vault_info to see available chains.`);
36
+ return match;
37
+ }
38
+ function parseChainToken(input, knownChains) {
39
+ const parts = input.split(":");
40
+ const chain = resolveChain(parts[0], knownChains);
41
+ return { chain, symbol: parts[1] };
42
+ }
43
+ var READ_ONLY_TOOLS = ["get_balances", "get_portfolio", "get_address", "vault_info", "supported_chains", "swap_quote"];
44
+ function getTools(vault, profile = "defi") {
45
+ const knownChains = vault.chains;
46
+ const all = {
47
+ get_balances: {
48
+ description: descriptions.balance.description,
49
+ inputSchema: z.object({
50
+ chain: z.string().optional().describe(descriptions.balance.params.chain),
51
+ includeTokens: z.boolean().optional().describe(descriptions.balance.params.includeTokens)
52
+ }),
53
+ handler: (args) => wrapHandler(async () => {
54
+ const includeTokens = args.includeTokens ?? false;
55
+ const balances = await vault.allBalances(includeTokens);
56
+ const chainFilter = args.chain;
57
+ if (!chainFilter) return balances;
58
+ const target = resolveChain(chainFilter, knownChains);
59
+ return balances.filter((b) => b.chain === target);
60
+ })
61
+ },
62
+ get_portfolio: {
63
+ description: descriptions.portfolio.description,
64
+ inputSchema: z.object({
65
+ chain: z.string().optional().describe(descriptions.portfolio.params.chain)
66
+ }),
67
+ handler: (args) => wrapHandler(async () => {
68
+ const portfolio = await vault.portfolio();
69
+ const chainFilter = args.chain;
70
+ if (!chainFilter) return portfolio;
71
+ const target = resolveChain(chainFilter, knownChains);
72
+ return {
73
+ ...portfolio,
74
+ balances: portfolio.balances.filter((b) => b.chain === target)
75
+ };
76
+ })
77
+ },
78
+ get_address: {
79
+ description: descriptions.address.description,
80
+ inputSchema: z.object({
81
+ chain: z.string().describe(descriptions.address.params.chain)
82
+ }),
83
+ handler: (args) => wrapHandler(async () => {
84
+ const chain = resolveChain(args.chain, knownChains);
85
+ const address = await vault.address(chain);
86
+ return { chain, address };
87
+ })
88
+ },
89
+ vault_info: {
90
+ description: descriptions.vaultInfo.description,
91
+ inputSchema: z.object({}),
92
+ handler: () => wrapHandler(async () => ({
93
+ name: vault.name,
94
+ type: vault.type,
95
+ chains: vault.chains,
96
+ signers: vault.signers,
97
+ localPartyId: vault.localPartyId,
98
+ threshold: vault.threshold,
99
+ createdAt: vault.createdAt
100
+ }))
101
+ },
102
+ supported_chains: {
103
+ description: descriptions.supportedChains.description,
104
+ inputSchema: z.object({}),
105
+ handler: () => wrapHandler(async () => {
106
+ const chains = vault.getSupportedSwapChains();
107
+ return { chains };
108
+ })
109
+ },
110
+ swap_quote: {
111
+ description: descriptions.swapQuote.description,
112
+ inputSchema: z.object({
113
+ from: z.string().describe(descriptions.swapQuote.params.from),
114
+ to: z.string().describe(descriptions.swapQuote.params.to),
115
+ amount: z.string().describe(descriptions.swapQuote.params.amount)
116
+ }),
117
+ handler: (args) => wrapHandler(async () => {
118
+ const from = parseChainToken(args.from, knownChains);
119
+ const to = parseChainToken(args.to, knownChains);
120
+ return vault.swap({
121
+ fromChain: from.chain,
122
+ fromSymbol: from.symbol,
123
+ toChain: to.chain,
124
+ toSymbol: to.symbol,
125
+ amount: args.amount,
126
+ dryRun: true
127
+ });
128
+ })
129
+ },
130
+ send: {
131
+ description: descriptions.send.description,
132
+ inputSchema: z.object({
133
+ chain: z.string().describe(descriptions.send.params.chain),
134
+ to: z.string().describe(descriptions.send.params.to),
135
+ amount: z.string().describe(descriptions.send.params.amount),
136
+ token: z.string().optional().describe(descriptions.send.params.token),
137
+ memo: z.string().optional().describe(descriptions.send.params.memo),
138
+ confirmed: z.boolean().optional().describe(descriptions.send.params.confirmed)
139
+ }),
140
+ handler: (args) => wrapHandler(async () => {
141
+ const chain = resolveChain(args.chain, knownChains);
142
+ return vault.send({
143
+ chain,
144
+ to: args.to,
145
+ amount: args.amount,
146
+ symbol: args.token,
147
+ memo: args.memo,
148
+ dryRun: args.confirmed !== true
149
+ });
150
+ })
151
+ },
152
+ swap: {
153
+ description: descriptions.swap.description,
154
+ inputSchema: z.object({
155
+ from: z.string().describe(descriptions.swap.params.from),
156
+ to: z.string().describe(descriptions.swap.params.to),
157
+ amount: z.string().describe(descriptions.swap.params.amount),
158
+ confirmed: z.boolean().optional().describe(descriptions.swap.params.confirmed)
159
+ }),
160
+ handler: (args) => wrapHandler(async () => {
161
+ const from = parseChainToken(args.from, knownChains);
162
+ const to = parseChainToken(args.to, knownChains);
163
+ return vault.swap({
164
+ fromChain: from.chain,
165
+ fromSymbol: from.symbol,
166
+ toChain: to.chain,
167
+ toSymbol: to.symbol,
168
+ amount: args.amount,
169
+ dryRun: args.confirmed !== true
170
+ });
171
+ })
172
+ }
173
+ };
174
+ if (profile === "harness") {
175
+ const filtered = {};
176
+ for (const name of READ_ONLY_TOOLS) {
177
+ if (all[name]) filtered[name] = all[name];
178
+ }
179
+ return filtered;
180
+ }
181
+ return all;
182
+ }
183
+
184
+ // src/adapters/auth.ts
185
+ import { executeAuthSetup, executeAuthStatus, getDecryptionPassword, getServerPassword } from "@vultisig/client-shared";
186
+
187
+ // src/index.ts
188
+ var VERSION = "0.1.0";
189
+ function createMcpServer(vault, profile = "defi") {
190
+ const server = new McpServer({
191
+ name: "vultisig",
192
+ version: VERSION
193
+ });
194
+ const tools = getTools(vault, profile);
195
+ for (const [name, tool] of Object.entries(tools)) {
196
+ server.registerTool(
197
+ name,
198
+ {
199
+ description: tool.description,
200
+ inputSchema: tool.inputSchema
201
+ },
202
+ async (args) => tool.handler(args)
203
+ );
204
+ }
205
+ return server;
206
+ }
207
+ async function startMcpServer(vault, profile = "defi") {
208
+ const toStderr2 = (...args) => {
209
+ process.stderr.write(args.map(String).join(" ") + "\n");
210
+ };
211
+ console.log = toStderr2;
212
+ console.info = toStderr2;
213
+ console.warn = toStderr2;
214
+ const server = createMcpServer(vault, profile);
215
+ const transport = new StdioServerTransport();
216
+ await server.connect(transport);
217
+ }
218
+
219
+ // bin/mcp-server.ts
220
+ var toStderr = (...args) => {
221
+ process.stderr.write(args.map(String).join(" ") + "\n");
222
+ };
223
+ console.log = toStderr;
224
+ console.info = toStderr;
225
+ console.warn = toStderr;
226
+ var PROFILES = ["harness", "defi"];
227
+ function parseArgs() {
228
+ const args = process.argv.slice(2);
229
+ if (args.includes("--help") || args.includes("-h")) {
230
+ process.stderr.write(`
231
+ vultisig-mcp \u2014 MCP server for Vultisig wallet operations
232
+
233
+ SETUP (first time):
234
+ vmcp --setup Import a vault and store credentials interactively
235
+ vmcp --setup --vault-file <path> Import a specific vault file
236
+
237
+ USAGE:
238
+ vmcp [options]
239
+
240
+ OPTIONS:
241
+ --profile <harness|defi> Tool profile (default: defi)
242
+ harness = read-only tools only
243
+ defi = all tools including send/swap
244
+ --vault-id <id> Use a specific vault (default: first found)
245
+ --setup Run interactive auth setup, then exit
246
+
247
+ CI / HEADLESS (skip interactive auth):
248
+ --vault-file <path> Load vault directly from a .vult file
249
+ Requires VAULT_PASSWORD and/or VAULT_DECRYPT_PASSWORD
250
+ environment variables for encrypted vaults
251
+
252
+ EXAMPLES:
253
+ # First time setup:
254
+ vmcp --setup
255
+
256
+ # Start the server:
257
+ vmcp
258
+ vmcp --profile harness
259
+
260
+ # Claude Code integration:
261
+ claude mcp add vultisig -- vmcp
262
+ claude mcp add vultisig -- npx @vultisig/mcp
263
+
264
+ # CI / headless:
265
+ VAULT_PASSWORD=xxx vmcp --vault-file ./vault.vult
266
+ `);
267
+ process.exit(0);
268
+ }
269
+ let profile = "defi";
270
+ let vaultId;
271
+ let vaultFile;
272
+ const setup = args.includes("--setup");
273
+ for (let i = 0; i < args.length; i++) {
274
+ const arg = args[i];
275
+ if (arg === "--profile" && args[i + 1]) {
276
+ const value = args[++i];
277
+ if (!PROFILES.includes(value)) {
278
+ process.stderr.write(`Invalid profile "${value}". Must be one of: ${PROFILES.join(", ")}
279
+ `);
280
+ process.exit(1);
281
+ }
282
+ profile = value;
283
+ } else if (arg === "--vault-id" && args[i + 1]) {
284
+ vaultId = args[++i];
285
+ } else if (arg === "--vault-file" && args[i + 1]) {
286
+ vaultFile = args[++i];
287
+ }
288
+ }
289
+ return { profile, vaultId, vaultFile, setup };
290
+ }
291
+ async function runSetup(vaultFile) {
292
+ console.log = process.stdout.write.bind(process.stdout);
293
+ process.stderr.write("[vultisig-mcp] Running auth setup...\n");
294
+ const result = await executeAuthSetup2({ vaultFile });
295
+ process.stderr.write(
296
+ `[vultisig-mcp] Auth complete: vault "${result.vaultName}" (${result.storageBackend})
297
+ [vultisig-mcp] You can now start the server with: vmcp
298
+ `
299
+ );
300
+ }
301
+ async function main() {
302
+ const { profile, vaultId, vaultFile, setup } = parseArgs();
303
+ if (setup) {
304
+ await runSetup(vaultFile);
305
+ return;
306
+ }
307
+ const sdk = new Vultisig({
308
+ onPasswordRequired: async (vaultId2) => {
309
+ const password = await getServerPassword2(vaultId2);
310
+ if (!password) throw new Error("No server password found. Run: vmcp --setup");
311
+ return password;
312
+ }
313
+ });
314
+ await sdk.initialize();
315
+ let vault;
316
+ if (vaultFile) {
317
+ process.stderr.write(`[vultisig-mcp] Loading vault from file: ${vaultFile}
318
+ `);
319
+ const content = readFileSync(vaultFile, "utf-8");
320
+ vault = await sdk.importVault(content);
321
+ } else {
322
+ const vaults = await sdk.listVaults();
323
+ if (vaultId) {
324
+ vault = vaults.find((v) => v.id === vaultId);
325
+ if (!vault) {
326
+ const available = vaults.map((v) => ` - ${v.id} (${v.name})`).join("\n");
327
+ process.stderr.write(
328
+ `Vault "${vaultId}" not found.
329
+
330
+ ` + (vaults.length ? `Available vaults:
331
+ ${available}
332
+ ` : `No vaults imported. Run this first:
333
+ vmcp --setup
334
+ `)
335
+ );
336
+ process.exit(1);
337
+ }
338
+ } else {
339
+ vault = vaults[0];
340
+ if (!vault) {
341
+ process.stderr.write(
342
+ `No vaults found. You need to set up auth before starting the MCP server.
343
+
344
+ Run:
345
+ vmcp --setup
346
+
347
+ For CI/headless environments, use --vault-file instead:
348
+ VAULT_PASSWORD=xxx vmcp --vault-file ./vault.vult
349
+
350
+ Run vmcp --help for more options.
351
+ `
352
+ );
353
+ process.exit(1);
354
+ }
355
+ }
356
+ }
357
+ process.stderr.write(`[vultisig-mcp] Vault loaded: ${vault.name} (${vault.type})
358
+ `);
359
+ process.stderr.write(`[vultisig-mcp] Profile: ${profile} | Tools: ${profile === "harness" ? "read-only" : "all"}
360
+ `);
361
+ await startMcpServer(vault, profile);
362
+ }
363
+ main().catch((err) => {
364
+ process.stderr.write(`${err}
365
+ `);
366
+ process.exit(1);
367
+ });
@@ -0,0 +1,223 @@
1
+ // src/index.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+
5
+ // src/tools.ts
6
+ import { descriptions } from "@vultisig/client-shared";
7
+ import * as z from "zod/v4";
8
+ function success(data) {
9
+ return {
10
+ content: [{ type: "text", text: JSON.stringify(data, (_k, v) => typeof v === "bigint" ? v.toString() : v) }]
11
+ };
12
+ }
13
+ function error(message, hint) {
14
+ const payload = { error: message };
15
+ if (hint) payload.hint = hint;
16
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: true };
17
+ }
18
+ async function wrapHandler(fn) {
19
+ try {
20
+ return success(await fn());
21
+ } catch (err) {
22
+ if (err instanceof Error) return error(err.message);
23
+ return error(String(err));
24
+ }
25
+ }
26
+ function resolveChain(name, knownChains) {
27
+ const match = knownChains.find((c) => c.toLowerCase() === name.toLowerCase());
28
+ if (!match) throw new Error(`Unknown chain "${name}". Use supported_chains or vault_info to see available chains.`);
29
+ return match;
30
+ }
31
+ function parseChainToken(input, knownChains) {
32
+ const parts = input.split(":");
33
+ const chain = resolveChain(parts[0], knownChains);
34
+ return { chain, symbol: parts[1] };
35
+ }
36
+ var READ_ONLY_TOOLS = ["get_balances", "get_portfolio", "get_address", "vault_info", "supported_chains", "swap_quote"];
37
+ var WRITE_TOOLS = ["send", "swap"];
38
+ function getTools(vault, profile = "defi") {
39
+ const knownChains = vault.chains;
40
+ const all = {
41
+ get_balances: {
42
+ description: descriptions.balance.description,
43
+ inputSchema: z.object({
44
+ chain: z.string().optional().describe(descriptions.balance.params.chain),
45
+ includeTokens: z.boolean().optional().describe(descriptions.balance.params.includeTokens)
46
+ }),
47
+ handler: (args) => wrapHandler(async () => {
48
+ const includeTokens = args.includeTokens ?? false;
49
+ const balances = await vault.allBalances(includeTokens);
50
+ const chainFilter = args.chain;
51
+ if (!chainFilter) return balances;
52
+ const target = resolveChain(chainFilter, knownChains);
53
+ return balances.filter((b) => b.chain === target);
54
+ })
55
+ },
56
+ get_portfolio: {
57
+ description: descriptions.portfolio.description,
58
+ inputSchema: z.object({
59
+ chain: z.string().optional().describe(descriptions.portfolio.params.chain)
60
+ }),
61
+ handler: (args) => wrapHandler(async () => {
62
+ const portfolio = await vault.portfolio();
63
+ const chainFilter = args.chain;
64
+ if (!chainFilter) return portfolio;
65
+ const target = resolveChain(chainFilter, knownChains);
66
+ return {
67
+ ...portfolio,
68
+ balances: portfolio.balances.filter((b) => b.chain === target)
69
+ };
70
+ })
71
+ },
72
+ get_address: {
73
+ description: descriptions.address.description,
74
+ inputSchema: z.object({
75
+ chain: z.string().describe(descriptions.address.params.chain)
76
+ }),
77
+ handler: (args) => wrapHandler(async () => {
78
+ const chain = resolveChain(args.chain, knownChains);
79
+ const address = await vault.address(chain);
80
+ return { chain, address };
81
+ })
82
+ },
83
+ vault_info: {
84
+ description: descriptions.vaultInfo.description,
85
+ inputSchema: z.object({}),
86
+ handler: () => wrapHandler(async () => ({
87
+ name: vault.name,
88
+ type: vault.type,
89
+ chains: vault.chains,
90
+ signers: vault.signers,
91
+ localPartyId: vault.localPartyId,
92
+ threshold: vault.threshold,
93
+ createdAt: vault.createdAt
94
+ }))
95
+ },
96
+ supported_chains: {
97
+ description: descriptions.supportedChains.description,
98
+ inputSchema: z.object({}),
99
+ handler: () => wrapHandler(async () => {
100
+ const chains = vault.getSupportedSwapChains();
101
+ return { chains };
102
+ })
103
+ },
104
+ swap_quote: {
105
+ description: descriptions.swapQuote.description,
106
+ inputSchema: z.object({
107
+ from: z.string().describe(descriptions.swapQuote.params.from),
108
+ to: z.string().describe(descriptions.swapQuote.params.to),
109
+ amount: z.string().describe(descriptions.swapQuote.params.amount)
110
+ }),
111
+ handler: (args) => wrapHandler(async () => {
112
+ const from = parseChainToken(args.from, knownChains);
113
+ const to = parseChainToken(args.to, knownChains);
114
+ return vault.swap({
115
+ fromChain: from.chain,
116
+ fromSymbol: from.symbol,
117
+ toChain: to.chain,
118
+ toSymbol: to.symbol,
119
+ amount: args.amount,
120
+ dryRun: true
121
+ });
122
+ })
123
+ },
124
+ send: {
125
+ description: descriptions.send.description,
126
+ inputSchema: z.object({
127
+ chain: z.string().describe(descriptions.send.params.chain),
128
+ to: z.string().describe(descriptions.send.params.to),
129
+ amount: z.string().describe(descriptions.send.params.amount),
130
+ token: z.string().optional().describe(descriptions.send.params.token),
131
+ memo: z.string().optional().describe(descriptions.send.params.memo),
132
+ confirmed: z.boolean().optional().describe(descriptions.send.params.confirmed)
133
+ }),
134
+ handler: (args) => wrapHandler(async () => {
135
+ const chain = resolveChain(args.chain, knownChains);
136
+ return vault.send({
137
+ chain,
138
+ to: args.to,
139
+ amount: args.amount,
140
+ symbol: args.token,
141
+ memo: args.memo,
142
+ dryRun: args.confirmed !== true
143
+ });
144
+ })
145
+ },
146
+ swap: {
147
+ description: descriptions.swap.description,
148
+ inputSchema: z.object({
149
+ from: z.string().describe(descriptions.swap.params.from),
150
+ to: z.string().describe(descriptions.swap.params.to),
151
+ amount: z.string().describe(descriptions.swap.params.amount),
152
+ confirmed: z.boolean().optional().describe(descriptions.swap.params.confirmed)
153
+ }),
154
+ handler: (args) => wrapHandler(async () => {
155
+ const from = parseChainToken(args.from, knownChains);
156
+ const to = parseChainToken(args.to, knownChains);
157
+ return vault.swap({
158
+ fromChain: from.chain,
159
+ fromSymbol: from.symbol,
160
+ toChain: to.chain,
161
+ toSymbol: to.symbol,
162
+ amount: args.amount,
163
+ dryRun: args.confirmed !== true
164
+ });
165
+ })
166
+ }
167
+ };
168
+ if (profile === "harness") {
169
+ const filtered = {};
170
+ for (const name of READ_ONLY_TOOLS) {
171
+ if (all[name]) filtered[name] = all[name];
172
+ }
173
+ return filtered;
174
+ }
175
+ return all;
176
+ }
177
+ function getToolNames(profile) {
178
+ if (profile === "harness") return [...READ_ONLY_TOOLS];
179
+ return [...READ_ONLY_TOOLS, ...WRITE_TOOLS];
180
+ }
181
+
182
+ // src/adapters/auth.ts
183
+ import { executeAuthSetup, executeAuthStatus, getDecryptionPassword, getServerPassword } from "@vultisig/client-shared";
184
+
185
+ // src/index.ts
186
+ var VERSION = "0.1.0";
187
+ function createMcpServer(vault, profile = "defi") {
188
+ const server = new McpServer({
189
+ name: "vultisig",
190
+ version: VERSION
191
+ });
192
+ const tools = getTools(vault, profile);
193
+ for (const [name, tool] of Object.entries(tools)) {
194
+ server.registerTool(
195
+ name,
196
+ {
197
+ description: tool.description,
198
+ inputSchema: tool.inputSchema
199
+ },
200
+ async (args) => tool.handler(args)
201
+ );
202
+ }
203
+ return server;
204
+ }
205
+ async function startMcpServer(vault, profile = "defi") {
206
+ const toStderr = (...args) => {
207
+ process.stderr.write(args.map(String).join(" ") + "\n");
208
+ };
209
+ console.log = toStderr;
210
+ console.info = toStderr;
211
+ console.warn = toStderr;
212
+ const server = createMcpServer(vault, profile);
213
+ const transport = new StdioServerTransport();
214
+ await server.connect(transport);
215
+ }
216
+ export {
217
+ createMcpServer,
218
+ executeAuthSetup,
219
+ executeAuthStatus,
220
+ getToolNames,
221
+ getTools,
222
+ startMcpServer
223
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@vultisig/mcp",
3
+ "version": "0.1.1",
4
+ "description": "MCP server for Vultisig SDK - Model Context Protocol wrapper over wallet operations",
5
+ "type": "module",
6
+ "bin": {
7
+ "vmcp": "./dist/bin/mcp-server.js",
8
+ "vultisig-mcp": "./dist/bin/mcp-server.js"
9
+ },
10
+ "main": "dist/src/index.js",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "node build.mjs",
18
+ "dev": "npx tsx src/index.ts",
19
+ "start": "node dist/bin/mcp-server.js",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "keywords": [
25
+ "vultisig",
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "wallet",
29
+ "blockchain"
30
+ ],
31
+ "author": {
32
+ "name": "Vultisig",
33
+ "email": "info@vultisig.com",
34
+ "url": "https://vultisig.com"
35
+ },
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/vultisig/vultisig-sdk.git",
40
+ "directory": "clients/mcp"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "~1.29.0",
44
+ "@napi-rs/keyring": "^1.1.6",
45
+ "@vultisig/client-shared": "^0.2.0",
46
+ "@vultisig/sdk": "^0.16.0",
47
+ "zod": "^4.3.6"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^25.5.0",
51
+ "esbuild": "^0.27.4",
52
+ "tsx": "^4.21.0",
53
+ "typescript": "^5.9.3",
54
+ "vitest": "^3.0.9"
55
+ },
56
+ "engines": {
57
+ "node": ">=20.0.0"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "registry": "https://registry.npmjs.org/"
62
+ }
63
+ }