@shroud-fi/mcp-server 0.1.2 → 0.1.3
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/package.json +11 -8
- package/src/auth.ts +129 -0
- package/src/bin/http.ts +26 -0
- package/src/bin/stdio.ts +16 -0
- package/src/config.ts +198 -0
- package/src/constants.ts +35 -0
- package/src/errors.ts +68 -0
- package/src/http.ts +217 -0
- package/src/index.ts +41 -0
- package/src/server.ts +103 -0
- package/src/stdio.ts +19 -0
- package/src/tools/balance.ts +66 -0
- package/src/tools/index.ts +42 -0
- package/src/tools/receive.ts +105 -0
- package/src/tools/register.ts +37 -0
- package/src/tools/schema.ts +25 -0
- package/src/tools/send-to-wallet.ts +70 -0
- package/src/tools/send.ts +73 -0
- package/src/tools/status.ts +40 -0
- package/src/tools/sweep.ts +125 -0
- package/src/tools/x402-pay.ts +78 -0
- package/src/tools/x402-serve.ts +142 -0
- package/src/types.ts +74 -0
- package/tsconfig.json +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shroud-fi/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Model Context Protocol server for ShroudFi. 9 tools any MCP-aware AI agent host can call — Claude Code, Cursor, Windsurf, Zed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shroudfi",
|
|
@@ -41,17 +41,20 @@
|
|
|
41
41
|
"shroudfi-mcp": "./dist/esm/bin/stdio.js"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
|
-
"dist"
|
|
44
|
+
"dist",
|
|
45
|
+
"src",
|
|
46
|
+
"tsconfig.json",
|
|
47
|
+
"README.md"
|
|
45
48
|
],
|
|
46
49
|
"dependencies": {
|
|
47
50
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
48
51
|
"zod": "^3.23.0",
|
|
49
|
-
"@shroud-fi/
|
|
50
|
-
"@shroud-fi/
|
|
51
|
-
"@shroud-fi/
|
|
52
|
-
"@shroud-fi/
|
|
53
|
-
"@shroud-fi/
|
|
54
|
-
"@shroud-fi/
|
|
52
|
+
"@shroud-fi/agent-runtime": "0.1.6",
|
|
53
|
+
"@shroud-fi/core": "0.1.3",
|
|
54
|
+
"@shroud-fi/x402": "0.1.5",
|
|
55
|
+
"@shroud-fi/transport": "0.1.4",
|
|
56
|
+
"@shroud-fi/scanning": "0.1.3",
|
|
57
|
+
"@shroud-fi/payments": "0.1.3"
|
|
55
58
|
},
|
|
56
59
|
"peerDependencies": {
|
|
57
60
|
"viem": "^2.21.0"
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EIP-191 challenge-response auth for the HTTP transport.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Client POSTs /auth/challenge with their wallet address.
|
|
6
|
+
* 2. Server returns a fresh nonce + a canonical message string.
|
|
7
|
+
* 3. Client signs message with personal_sign / signMessage (EIP-191).
|
|
8
|
+
* 4. Client sends every subsequent request with:
|
|
9
|
+
* X-Shroudfi-Wallet: <0x… EOA>
|
|
10
|
+
* X-Shroudfi-Nonce: <nonce returned at step 2>
|
|
11
|
+
* X-Shroudfi-Signature: <0x… hex sig>
|
|
12
|
+
* 5. Server recovers, checks address match + expiry + allow-list.
|
|
13
|
+
*
|
|
14
|
+
* The signature is single-shot — once verifyAndConsume returns true for a
|
|
15
|
+
* nonce, the cache entry is deleted, so the same signed challenge cannot be
|
|
16
|
+
* replayed.
|
|
17
|
+
*
|
|
18
|
+
* Privacy: signature bytes never appear in logs, error messages, or response
|
|
19
|
+
* bodies. Errors are short tags only.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { randomBytes } from 'node:crypto';
|
|
23
|
+
import { recoverMessageAddress } from 'viem';
|
|
24
|
+
import type { Hex } from 'viem';
|
|
25
|
+
import { EIP191_CHALLENGE_TTL_MS } from './constants.js';
|
|
26
|
+
import {
|
|
27
|
+
McpEip191ChallengeExpiredError,
|
|
28
|
+
McpEip191SignatureInvalidError,
|
|
29
|
+
McpUnauthorizedError,
|
|
30
|
+
} from './errors.js';
|
|
31
|
+
import type { Eip191Challenge } from './types.js';
|
|
32
|
+
|
|
33
|
+
export interface AuthCache {
|
|
34
|
+
issue(wallet: `0x${string}`): Eip191Challenge;
|
|
35
|
+
verifyAndConsume(args: {
|
|
36
|
+
wallet: `0x${string}`;
|
|
37
|
+
nonce: Hex;
|
|
38
|
+
signature: Hex;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
/** Test seam — drop expired entries. Called automatically on every op. */
|
|
41
|
+
prune(now?: number): void;
|
|
42
|
+
/** Returns the current size — test seam. */
|
|
43
|
+
size(): number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SHROUDFI_DOMAIN_MARK = 'ShroudFi MCP HTTP authentication';
|
|
47
|
+
|
|
48
|
+
function buildMessage(wallet: `0x${string}`, nonce: Hex, issuedAtMs: number): string {
|
|
49
|
+
// The message is deliberately self-describing so a wallet UI shows the
|
|
50
|
+
// user the exact intent before signing. The nonce is the only random
|
|
51
|
+
// piece — it ties the signature to a single server-issued challenge.
|
|
52
|
+
return [
|
|
53
|
+
SHROUDFI_DOMAIN_MARK,
|
|
54
|
+
`Wallet: ${wallet}`,
|
|
55
|
+
`Nonce: ${nonce}`,
|
|
56
|
+
`Issued: ${new Date(issuedAtMs).toISOString()}`,
|
|
57
|
+
'Sign to prove control of this wallet.',
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createAuthCache(
|
|
62
|
+
ttlMs: number = EIP191_CHALLENGE_TTL_MS,
|
|
63
|
+
): AuthCache {
|
|
64
|
+
const store = new Map<string, Eip191Challenge>();
|
|
65
|
+
|
|
66
|
+
function makeKey(wallet: `0x${string}`, nonce: Hex): string {
|
|
67
|
+
return `${wallet.toLowerCase()}|${nonce.toLowerCase()}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function prune(now: number = Date.now()): void {
|
|
71
|
+
for (const [k, v] of store) {
|
|
72
|
+
if (v.expiresAtMs <= now) store.delete(k);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
issue(wallet) {
|
|
78
|
+
prune();
|
|
79
|
+
const nonceBytes = randomBytes(16);
|
|
80
|
+
const nonce: Hex = `0x${nonceBytes.toString('hex')}`;
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const ch: Eip191Challenge = {
|
|
83
|
+
wallet: wallet.toLowerCase() as `0x${string}`,
|
|
84
|
+
nonce,
|
|
85
|
+
issuedAtMs: now,
|
|
86
|
+
expiresAtMs: now + ttlMs,
|
|
87
|
+
message: buildMessage(wallet.toLowerCase() as `0x${string}`, nonce, now),
|
|
88
|
+
};
|
|
89
|
+
store.set(makeKey(ch.wallet, ch.nonce), ch);
|
|
90
|
+
return ch;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async verifyAndConsume({ wallet, nonce, signature }) {
|
|
94
|
+
// Don't prune before lookup — we want callers to see the explicit
|
|
95
|
+
// `auth_challenge_expired` code when their nonce has timed out, instead
|
|
96
|
+
// of an indistinguishable `unauthorized`.
|
|
97
|
+
const key = makeKey(wallet, nonce);
|
|
98
|
+
const ch = store.get(key);
|
|
99
|
+
if (ch === undefined) {
|
|
100
|
+
throw new McpUnauthorizedError();
|
|
101
|
+
}
|
|
102
|
+
if (ch.expiresAtMs <= Date.now()) {
|
|
103
|
+
store.delete(key);
|
|
104
|
+
throw new McpEip191ChallengeExpiredError();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let recovered: `0x${string}`;
|
|
108
|
+
try {
|
|
109
|
+
recovered = await recoverMessageAddress({
|
|
110
|
+
message: ch.message,
|
|
111
|
+
signature,
|
|
112
|
+
});
|
|
113
|
+
} catch {
|
|
114
|
+
throw new McpEip191SignatureInvalidError();
|
|
115
|
+
}
|
|
116
|
+
if (recovered.toLowerCase() !== ch.wallet.toLowerCase()) {
|
|
117
|
+
throw new McpEip191SignatureInvalidError();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Single-use: consume the nonce so the signed challenge can't be replayed.
|
|
121
|
+
store.delete(key);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
prune,
|
|
125
|
+
size: () => store.size,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const __auth_test__ = { buildMessage };
|
package/src/bin/http.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* HTTP transport bin. Reads SHROUDFI_MCP_HTTP_PORT (default 7070) via the
|
|
4
|
+
* config module, boots the HTTP server, parks until SIGINT/SIGTERM.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { runHttpServer } from '../http.js';
|
|
8
|
+
import { readHttpPortFromEnv } from '../config.js';
|
|
9
|
+
|
|
10
|
+
const port = readHttpPortFromEnv();
|
|
11
|
+
|
|
12
|
+
runHttpServer({ port })
|
|
13
|
+
.then((handle) => {
|
|
14
|
+
process.stderr.write(`shroudfi-mcp-http listening on :${port}\n`);
|
|
15
|
+
const shutdown = (): void => {
|
|
16
|
+
void handle.close().then(() => process.exit(0));
|
|
17
|
+
};
|
|
18
|
+
process.on('SIGINT', shutdown);
|
|
19
|
+
process.on('SIGTERM', shutdown);
|
|
20
|
+
})
|
|
21
|
+
.catch((err: unknown) => {
|
|
22
|
+
process.stderr.write(
|
|
23
|
+
`shroudfi-mcp-http failed: ${(err as { code?: string }).code ?? 'unknown'}\n`,
|
|
24
|
+
);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
});
|
package/src/bin/stdio.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stdio bin — invoked by MCP clients via `.mcp.json` `command` field.
|
|
4
|
+
*
|
|
5
|
+
* Errors go to stderr only — stdout is reserved for the JSON-RPC framed
|
|
6
|
+
* MCP protocol stream. Any stdout pollution corrupts the client.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runStdioServer } from '../stdio.js';
|
|
10
|
+
|
|
11
|
+
runStdioServer().catch((err: unknown) => {
|
|
12
|
+
process.stderr.write(
|
|
13
|
+
`shroudfi-mcp stdio failed: ${(err as { code?: string }).code ?? 'unknown'}\n`,
|
|
14
|
+
);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve operator config from env (or explicit args) into a runtime context.
|
|
3
|
+
*
|
|
4
|
+
* Env reads concentrated here per verify-privacy R5. Allowed to use process.env.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import type { Hex } from 'viem';
|
|
9
|
+
import { hexToBytes, isHex } from 'viem';
|
|
10
|
+
import { createTransport, getShroudFiStealth } from '@shroud-fi/transport';
|
|
11
|
+
import { createShroudAgent } from '@shroud-fi/agent-runtime';
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_RPC_URL_BY_CHAIN,
|
|
14
|
+
ENV_CHAIN,
|
|
15
|
+
ENV_HTTP_ALLOWED_WALLETS,
|
|
16
|
+
ENV_HTTP_PORT,
|
|
17
|
+
ENV_MASTER_SEED,
|
|
18
|
+
ENV_MASTER_SEED_FILE,
|
|
19
|
+
ENV_PRIVATE_KEY,
|
|
20
|
+
ENV_PRIVATE_KEY_FILE,
|
|
21
|
+
ENV_RPC_URL,
|
|
22
|
+
ENV_START_BLOCK,
|
|
23
|
+
} from './constants.js';
|
|
24
|
+
import { McpConfigError } from './errors.js';
|
|
25
|
+
import type {
|
|
26
|
+
McpServerBootstrapConfig,
|
|
27
|
+
McpServerContext,
|
|
28
|
+
} from './types.js';
|
|
29
|
+
|
|
30
|
+
function readChain(): number {
|
|
31
|
+
const v = process.env[ENV_CHAIN];
|
|
32
|
+
if (v === undefined || v === '') return 8453;
|
|
33
|
+
if (v === 'base' || v === 'mainnet' || v === '8453') return 8453;
|
|
34
|
+
if (v === 'sepolia' || v === 'base-sepolia' || v === '84532') return 84532;
|
|
35
|
+
throw new McpConfigError('Unknown chain — set SHROUDFI_CHAIN to base|sepolia');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readPrivateKey(): Hex | undefined {
|
|
39
|
+
const inline = process.env[ENV_PRIVATE_KEY];
|
|
40
|
+
if (typeof inline === 'string' && inline.length > 0) {
|
|
41
|
+
if (!isHex(inline) || inline.length !== 66) {
|
|
42
|
+
throw new McpConfigError('SHROUDFI_PRIVATE_KEY is not a 0x… 32-byte hex');
|
|
43
|
+
}
|
|
44
|
+
return inline as Hex;
|
|
45
|
+
}
|
|
46
|
+
const file = process.env[ENV_PRIVATE_KEY_FILE];
|
|
47
|
+
if (typeof file === 'string' && file.length > 0) {
|
|
48
|
+
let raw: string;
|
|
49
|
+
try {
|
|
50
|
+
raw = readFileSync(file, 'utf-8').trim();
|
|
51
|
+
} catch {
|
|
52
|
+
throw new McpConfigError('SHROUDFI_PRIVATE_KEY_FILE could not be read');
|
|
53
|
+
}
|
|
54
|
+
if (!isHex(raw) || raw.length !== 66) {
|
|
55
|
+
throw new McpConfigError('Private key file contents are not a 0x… 32-byte hex');
|
|
56
|
+
}
|
|
57
|
+
return raw as Hex;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the optional 32-byte master seed from env. Accepts either an inline
|
|
64
|
+
* 0x… hex string or a path to a file containing one (same shape as the
|
|
65
|
+
* private-key resolution). Returns `undefined` when neither is set so callers
|
|
66
|
+
* can fall back to per-process random seeds.
|
|
67
|
+
*/
|
|
68
|
+
function readMasterSeed(): Uint8Array | undefined {
|
|
69
|
+
const inline = process.env[ENV_MASTER_SEED];
|
|
70
|
+
if (typeof inline === 'string' && inline.length > 0) {
|
|
71
|
+
if (!isHex(inline) || inline.length !== 66) {
|
|
72
|
+
throw new McpConfigError('SHROUDFI_MASTER_SEED is not a 0x… 32-byte hex');
|
|
73
|
+
}
|
|
74
|
+
return hexToBytes(inline as Hex);
|
|
75
|
+
}
|
|
76
|
+
const file = process.env[ENV_MASTER_SEED_FILE];
|
|
77
|
+
if (typeof file === 'string' && file.length > 0) {
|
|
78
|
+
let raw: string;
|
|
79
|
+
try {
|
|
80
|
+
raw = readFileSync(file, 'utf-8').trim();
|
|
81
|
+
} catch {
|
|
82
|
+
throw new McpConfigError('SHROUDFI_MASTER_SEED_FILE could not be read');
|
|
83
|
+
}
|
|
84
|
+
if (!isHex(raw) || raw.length !== 66) {
|
|
85
|
+
throw new McpConfigError('Master seed file contents are not a 0x… 32-byte hex');
|
|
86
|
+
}
|
|
87
|
+
return hexToBytes(raw as Hex);
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readStartBlock(): bigint {
|
|
93
|
+
const v = process.env[ENV_START_BLOCK];
|
|
94
|
+
if (v === undefined || v === '') return 0n;
|
|
95
|
+
try {
|
|
96
|
+
return BigInt(v);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new McpConfigError('SHROUDFI_START_BLOCK must be a non-negative integer');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readHttpAllowedWallets(): ReadonlySet<`0x${string}`> {
|
|
103
|
+
const v = process.env[ENV_HTTP_ALLOWED_WALLETS];
|
|
104
|
+
if (v === undefined || v === '') return new Set();
|
|
105
|
+
const out = new Set<`0x${string}`>();
|
|
106
|
+
for (const piece of v.split(',')) {
|
|
107
|
+
const trimmed = piece.trim().toLowerCase();
|
|
108
|
+
if (trimmed.length === 0) continue;
|
|
109
|
+
if (!isHex(trimmed) || trimmed.length !== 42) {
|
|
110
|
+
throw new McpConfigError(
|
|
111
|
+
'SHROUDFI_MCP_HTTP_ALLOWED_WALLETS contains a non-address entry',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
out.add(trimmed as `0x${string}`);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read the HTTP port the operator wants the MCP server to listen on. Defaults
|
|
121
|
+
* to 7070. Strict integer between 1 and 65535.
|
|
122
|
+
*/
|
|
123
|
+
export function readHttpPortFromEnv(defaultPort = 7070): number {
|
|
124
|
+
const raw = process.env[ENV_HTTP_PORT];
|
|
125
|
+
if (raw === undefined || raw.length === 0) return defaultPort;
|
|
126
|
+
const port = Number(raw);
|
|
127
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
128
|
+
throw new McpConfigError('SHROUDFI_MCP_HTTP_PORT must be 1..65535');
|
|
129
|
+
}
|
|
130
|
+
return port;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve env into a fully-formed bootstrap config.
|
|
135
|
+
*/
|
|
136
|
+
export function loadBootstrapConfigFromEnv(): McpServerBootstrapConfig {
|
|
137
|
+
const chainId = readChain();
|
|
138
|
+
const rpcOverride = process.env[ENV_RPC_URL];
|
|
139
|
+
const rpcUrl =
|
|
140
|
+
rpcOverride !== undefined && rpcOverride.length > 0
|
|
141
|
+
? rpcOverride
|
|
142
|
+
: DEFAULT_RPC_URL_BY_CHAIN[chainId];
|
|
143
|
+
if (rpcUrl === undefined) {
|
|
144
|
+
throw new McpConfigError('No RPC URL resolved for the requested chain');
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
chainId,
|
|
148
|
+
rpcUrl,
|
|
149
|
+
privateKey: readPrivateKey(),
|
|
150
|
+
startBlock: readStartBlock(),
|
|
151
|
+
masterSeed: readMasterSeed(),
|
|
152
|
+
httpAllowedWallets: readHttpAllowedWallets(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build a McpServerContext from a resolved bootstrap config.
|
|
158
|
+
*
|
|
159
|
+
* Throws McpConfigError if the chain is unknown to @shroud-fi/transport.
|
|
160
|
+
*/
|
|
161
|
+
export function buildContextFromConfig(
|
|
162
|
+
config: McpServerBootstrapConfig,
|
|
163
|
+
): McpServerContext {
|
|
164
|
+
const chainName = config.chainId === 8453 ? 'base' : 'baseSepolia';
|
|
165
|
+
|
|
166
|
+
const transport = createTransport({
|
|
167
|
+
chain: chainName as 'base' | 'baseSepolia',
|
|
168
|
+
rpcUrl: config.rpcUrl,
|
|
169
|
+
...(config.privateKey !== undefined
|
|
170
|
+
? { privateKey: config.privateKey }
|
|
171
|
+
: {}),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Will throw UnknownChainError if no manifest entry — surface as config error.
|
|
175
|
+
let stealthContract: `0x${string}`;
|
|
176
|
+
try {
|
|
177
|
+
stealthContract = getShroudFiStealth(config.chainId);
|
|
178
|
+
} catch {
|
|
179
|
+
throw new McpConfigError('No ShroudFiStealth deployment for the chain');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const agent = createShroudAgent({
|
|
183
|
+
transport,
|
|
184
|
+
startBlock: config.startBlock,
|
|
185
|
+
stealthContract,
|
|
186
|
+
autoRegister: true,
|
|
187
|
+
...(config.masterSeed !== undefined ? { masterSeed: config.masterSeed } : {}),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const walletAddress = transport.walletClient?.account?.address;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
agent,
|
|
194
|
+
transport,
|
|
195
|
+
chainId: config.chainId,
|
|
196
|
+
walletAddress,
|
|
197
|
+
};
|
|
198
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static constants. All env reads live here per verify-privacy R5.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SHROUDFI_MCP_SERVER_NAME = 'shroudfi-mcp' as const;
|
|
6
|
+
export const SHROUDFI_MCP_SERVER_VERSION = '0.0.1' as const;
|
|
7
|
+
|
|
8
|
+
/** EIP-191 challenge TTL — short window so a replayed challenge dies fast. */
|
|
9
|
+
export const EIP191_CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
/** Default RPC URLs by chain id. Operator override via SHROUDFI_RPC_URL. */
|
|
12
|
+
export const DEFAULT_RPC_URL_BY_CHAIN: Record<number, string> = {
|
|
13
|
+
8453: 'https://mainnet.base.org',
|
|
14
|
+
84532: 'https://sepolia.base.org',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Env knobs the bootstrap surface reads. */
|
|
18
|
+
export const ENV_PRIVATE_KEY = 'SHROUDFI_PRIVATE_KEY' as const;
|
|
19
|
+
export const ENV_PRIVATE_KEY_FILE = 'SHROUDFI_PRIVATE_KEY_FILE' as const;
|
|
20
|
+
export const ENV_RPC_URL = 'SHROUDFI_RPC_URL' as const;
|
|
21
|
+
export const ENV_CHAIN = 'SHROUDFI_CHAIN' as const;
|
|
22
|
+
export const ENV_START_BLOCK = 'SHROUDFI_START_BLOCK' as const;
|
|
23
|
+
export const ENV_HTTP_PORT = 'SHROUDFI_MCP_HTTP_PORT' as const;
|
|
24
|
+
export const ENV_HTTP_ALLOWED_WALLETS = 'SHROUDFI_MCP_HTTP_ALLOWED_WALLETS' as const;
|
|
25
|
+
/**
|
|
26
|
+
* Deterministic 32-byte master seed (0x… 64 hex chars). When set, every
|
|
27
|
+
* boot of the MCP server, REST API, or any other surface using
|
|
28
|
+
* `loadBootstrapConfigFromEnv` resolves the SAME stealth meta-address for
|
|
29
|
+
* the same operator EOA. When unset, a fresh random seed is generated per
|
|
30
|
+
* process — fine for local testing, BAD for production multi-surface
|
|
31
|
+
* deployments (caller's main wallet ↔ stealth meta-address binding would
|
|
32
|
+
* desync between MCP, REST, and UI processes).
|
|
33
|
+
*/
|
|
34
|
+
export const ENV_MASTER_SEED = 'SHROUDFI_MASTER_SEED' as const;
|
|
35
|
+
export const ENV_MASTER_SEED_FILE = 'SHROUDFI_MASTER_SEED_FILE' as const;
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors for @shroud-fi/mcp-server.
|
|
3
|
+
*
|
|
4
|
+
* Privacy invariants:
|
|
5
|
+
* - No private keys, signatures, or amounts in error messages.
|
|
6
|
+
* - No wallet addresses concatenated into `.message` strings — addresses
|
|
7
|
+
* are exposed on dedicated structured fields when needed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class McpServerError extends Error {
|
|
11
|
+
override readonly name: string = 'McpServerError';
|
|
12
|
+
readonly code: string;
|
|
13
|
+
constructor(message: string, code: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class McpConfigError extends McpServerError {
|
|
20
|
+
override readonly name = 'McpConfigError';
|
|
21
|
+
constructor(message: string) {
|
|
22
|
+
super(message, 'config_invalid');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class McpUnauthorizedError extends McpServerError {
|
|
27
|
+
override readonly name = 'McpUnauthorizedError';
|
|
28
|
+
constructor() {
|
|
29
|
+
super('Unauthorized', 'unauthorized');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class McpToolNotFoundError extends McpServerError {
|
|
34
|
+
override readonly name = 'McpToolNotFoundError';
|
|
35
|
+
constructor() {
|
|
36
|
+
super('Tool not found', 'tool_not_found');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class McpInvalidArgsError extends McpServerError {
|
|
41
|
+
override readonly name = 'McpInvalidArgsError';
|
|
42
|
+
constructor() {
|
|
43
|
+
super('Invalid tool arguments', 'invalid_args');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class McpExecutionError extends McpServerError {
|
|
48
|
+
override readonly name = 'McpExecutionError';
|
|
49
|
+
readonly cause: unknown;
|
|
50
|
+
constructor(cause: unknown) {
|
|
51
|
+
super('Tool execution failed', 'execution_failed');
|
|
52
|
+
this.cause = cause;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class McpEip191ChallengeExpiredError extends McpServerError {
|
|
57
|
+
override readonly name = 'McpEip191ChallengeExpiredError';
|
|
58
|
+
constructor() {
|
|
59
|
+
super('EIP-191 challenge expired', 'auth_challenge_expired');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class McpEip191SignatureInvalidError extends McpServerError {
|
|
64
|
+
override readonly name = 'McpEip191SignatureInvalidError';
|
|
65
|
+
constructor() {
|
|
66
|
+
super('EIP-191 signature invalid', 'auth_signature_invalid');
|
|
67
|
+
}
|
|
68
|
+
}
|