@shroud-fi/mcp-server 0.1.1 → 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.
Files changed (115) hide show
  1. package/package.json +11 -8
  2. package/src/auth.ts +129 -0
  3. package/src/bin/http.ts +26 -0
  4. package/src/bin/stdio.ts +16 -0
  5. package/src/config.ts +198 -0
  6. package/src/constants.ts +35 -0
  7. package/src/errors.ts +68 -0
  8. package/src/http.ts +217 -0
  9. package/src/index.ts +41 -0
  10. package/src/server.ts +103 -0
  11. package/src/stdio.ts +19 -0
  12. package/src/tools/balance.ts +66 -0
  13. package/src/tools/index.ts +42 -0
  14. package/src/tools/receive.ts +105 -0
  15. package/src/tools/register.ts +37 -0
  16. package/src/tools/schema.ts +25 -0
  17. package/src/tools/send-to-wallet.ts +70 -0
  18. package/src/tools/send.ts +73 -0
  19. package/src/tools/status.ts +40 -0
  20. package/src/tools/sweep.ts +125 -0
  21. package/src/tools/x402-pay.ts +78 -0
  22. package/src/tools/x402-serve.ts +142 -0
  23. package/src/types.ts +74 -0
  24. package/tsconfig.json +9 -0
  25. package/dist/cjs/auth.d.ts.map +0 -1
  26. package/dist/cjs/auth.js.map +0 -1
  27. package/dist/cjs/bin/http.d.ts.map +0 -1
  28. package/dist/cjs/bin/http.js.map +0 -1
  29. package/dist/cjs/bin/stdio.d.ts.map +0 -1
  30. package/dist/cjs/bin/stdio.js.map +0 -1
  31. package/dist/cjs/config.d.ts.map +0 -1
  32. package/dist/cjs/config.js.map +0 -1
  33. package/dist/cjs/constants.d.ts.map +0 -1
  34. package/dist/cjs/constants.js.map +0 -1
  35. package/dist/cjs/errors.d.ts.map +0 -1
  36. package/dist/cjs/errors.js.map +0 -1
  37. package/dist/cjs/http.d.ts.map +0 -1
  38. package/dist/cjs/http.js.map +0 -1
  39. package/dist/cjs/index.d.ts.map +0 -1
  40. package/dist/cjs/index.js.map +0 -1
  41. package/dist/cjs/server.d.ts.map +0 -1
  42. package/dist/cjs/server.js.map +0 -1
  43. package/dist/cjs/stdio.d.ts.map +0 -1
  44. package/dist/cjs/stdio.js.map +0 -1
  45. package/dist/cjs/tools/balance.d.ts.map +0 -1
  46. package/dist/cjs/tools/balance.js.map +0 -1
  47. package/dist/cjs/tools/index.d.ts.map +0 -1
  48. package/dist/cjs/tools/index.js.map +0 -1
  49. package/dist/cjs/tools/receive.d.ts.map +0 -1
  50. package/dist/cjs/tools/receive.js.map +0 -1
  51. package/dist/cjs/tools/register.d.ts.map +0 -1
  52. package/dist/cjs/tools/register.js.map +0 -1
  53. package/dist/cjs/tools/schema.d.ts.map +0 -1
  54. package/dist/cjs/tools/schema.js.map +0 -1
  55. package/dist/cjs/tools/send-to-wallet.d.ts.map +0 -1
  56. package/dist/cjs/tools/send-to-wallet.js.map +0 -1
  57. package/dist/cjs/tools/send.d.ts.map +0 -1
  58. package/dist/cjs/tools/send.js.map +0 -1
  59. package/dist/cjs/tools/status.d.ts.map +0 -1
  60. package/dist/cjs/tools/status.js.map +0 -1
  61. package/dist/cjs/tools/sweep.d.ts.map +0 -1
  62. package/dist/cjs/tools/sweep.js.map +0 -1
  63. package/dist/cjs/tools/x402-pay.d.ts.map +0 -1
  64. package/dist/cjs/tools/x402-pay.js.map +0 -1
  65. package/dist/cjs/tools/x402-serve.d.ts.map +0 -1
  66. package/dist/cjs/tools/x402-serve.js.map +0 -1
  67. package/dist/cjs/types.d.ts.map +0 -1
  68. package/dist/cjs/types.js.map +0 -1
  69. package/dist/esm/auth.d.ts.map +0 -1
  70. package/dist/esm/auth.js.map +0 -1
  71. package/dist/esm/bin/http.d.ts.map +0 -1
  72. package/dist/esm/bin/http.js.map +0 -1
  73. package/dist/esm/bin/stdio.d.ts.map +0 -1
  74. package/dist/esm/bin/stdio.js.map +0 -1
  75. package/dist/esm/config.d.ts.map +0 -1
  76. package/dist/esm/config.js.map +0 -1
  77. package/dist/esm/constants.d.ts.map +0 -1
  78. package/dist/esm/constants.js.map +0 -1
  79. package/dist/esm/errors.d.ts.map +0 -1
  80. package/dist/esm/errors.js.map +0 -1
  81. package/dist/esm/http.d.ts.map +0 -1
  82. package/dist/esm/http.js.map +0 -1
  83. package/dist/esm/index.d.ts.map +0 -1
  84. package/dist/esm/index.js.map +0 -1
  85. package/dist/esm/server.d.ts.map +0 -1
  86. package/dist/esm/server.js.map +0 -1
  87. package/dist/esm/stdio.d.ts.map +0 -1
  88. package/dist/esm/stdio.js.map +0 -1
  89. package/dist/esm/tools/balance.d.ts.map +0 -1
  90. package/dist/esm/tools/balance.js.map +0 -1
  91. package/dist/esm/tools/index.d.ts.map +0 -1
  92. package/dist/esm/tools/index.js.map +0 -1
  93. package/dist/esm/tools/receive.d.ts.map +0 -1
  94. package/dist/esm/tools/receive.js.map +0 -1
  95. package/dist/esm/tools/register.d.ts.map +0 -1
  96. package/dist/esm/tools/register.js.map +0 -1
  97. package/dist/esm/tools/schema.d.ts.map +0 -1
  98. package/dist/esm/tools/schema.js.map +0 -1
  99. package/dist/esm/tools/send-to-wallet.d.ts.map +0 -1
  100. package/dist/esm/tools/send-to-wallet.js.map +0 -1
  101. package/dist/esm/tools/send.d.ts.map +0 -1
  102. package/dist/esm/tools/send.js.map +0 -1
  103. package/dist/esm/tools/status.d.ts.map +0 -1
  104. package/dist/esm/tools/status.js.map +0 -1
  105. package/dist/esm/tools/sweep.d.ts.map +0 -1
  106. package/dist/esm/tools/sweep.js.map +0 -1
  107. package/dist/esm/tools/x402-pay.d.ts.map +0 -1
  108. package/dist/esm/tools/x402-pay.js.map +0 -1
  109. package/dist/esm/tools/x402-serve.d.ts.map +0 -1
  110. package/dist/esm/tools/x402-serve.js.map +0 -1
  111. package/dist/esm/types.d.ts.map +0 -1
  112. package/dist/esm/types.js.map +0 -1
  113. package/dist/tsconfig.cjs.tsbuildinfo +0 -1
  114. package/dist/tsconfig.esm.tsbuildinfo +0 -1
  115. package/dist/tsconfig.tsbuildinfo +0 -1
package/src/http.ts ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * HTTP transport for the MCP server.
3
+ *
4
+ * Uses node:http to stay zero-dep. Exposes three endpoints:
5
+ * POST /auth/challenge → mint EIP-191 challenge
6
+ * POST /mcp → JSON-RPC framed MCP body (auth-gated)
7
+ * GET /healthz → public liveness ping
8
+ *
9
+ * Auth rules:
10
+ * - /auth/challenge public (clients need a way to get a challenge)
11
+ * - /mcp requires X-Shroudfi-Wallet + X-Shroudfi-Nonce +
12
+ * X-Shroudfi-Signature headers. Signature is verified,
13
+ * consumed (single-use), allow-list checked.
14
+ * - /healthz public
15
+ *
16
+ * Privacy invariants:
17
+ * - Signatures never logged or echoed.
18
+ * - Errors are short tag strings; no body bytes, no key bytes.
19
+ * - Allow-list is enforced when SHROUDFI_MCP_HTTP_ALLOWED_WALLETS is set;
20
+ * when empty, ANY EIP-191 wallet may authenticate. Operators MUST set
21
+ * the allow-list in production.
22
+ */
23
+
24
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
25
+ import type { Hex } from 'viem';
26
+ import { isHex } from 'viem';
27
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
28
+ import { ALL_TOOLS, findTool } from './tools/index.js';
29
+ import { createAuthCache, type AuthCache } from './auth.js';
30
+ import { buildContextFromConfig, loadBootstrapConfigFromEnv } from './config.js';
31
+ import type { McpServerBootstrapConfig, McpServerContext } from './types.js';
32
+ import { McpInvalidArgsError, McpServerError, McpUnauthorizedError } from './errors.js';
33
+
34
+ const HEADER_WALLET = 'x-shroudfi-wallet';
35
+ const HEADER_NONCE = 'x-shroudfi-nonce';
36
+ const HEADER_SIGNATURE = 'x-shroudfi-signature';
37
+
38
+ const JSON_HEADERS = { 'content-type': 'application/json' } as const;
39
+
40
+ interface RunHttpServerOptions {
41
+ readonly port: number;
42
+ readonly bootstrap?: McpServerBootstrapConfig;
43
+ }
44
+
45
+ async function readJsonBody(req: IncomingMessage): Promise<unknown> {
46
+ const chunks: Buffer[] = [];
47
+ for await (const chunk of req) {
48
+ chunks.push(chunk as Buffer);
49
+ if (Buffer.concat(chunks).length > 1_048_576) {
50
+ throw new McpInvalidArgsError();
51
+ }
52
+ }
53
+ const raw = Buffer.concat(chunks).toString('utf-8');
54
+ if (raw.length === 0) return {};
55
+ try {
56
+ return JSON.parse(raw);
57
+ } catch {
58
+ throw new McpInvalidArgsError();
59
+ }
60
+ }
61
+
62
+ function send(res: ServerResponse, status: number, body: unknown): void {
63
+ res.writeHead(status, JSON_HEADERS);
64
+ res.end(JSON.stringify(body));
65
+ }
66
+
67
+ async function authenticate(
68
+ req: IncomingMessage,
69
+ authCache: AuthCache,
70
+ allowList: ReadonlySet<`0x${string}`>,
71
+ ): Promise<`0x${string}`> {
72
+ const wallet = req.headers[HEADER_WALLET];
73
+ const nonce = req.headers[HEADER_NONCE];
74
+ const sig = req.headers[HEADER_SIGNATURE];
75
+ if (
76
+ typeof wallet !== 'string' ||
77
+ typeof nonce !== 'string' ||
78
+ typeof sig !== 'string'
79
+ ) {
80
+ throw new McpUnauthorizedError();
81
+ }
82
+ if (!isHex(wallet) || wallet.length !== 42) throw new McpUnauthorizedError();
83
+ if (!isHex(nonce)) throw new McpUnauthorizedError();
84
+ if (!isHex(sig)) throw new McpUnauthorizedError();
85
+ const walletLc = wallet.toLowerCase() as `0x${string}`;
86
+ if (allowList.size > 0 && !allowList.has(walletLc)) {
87
+ throw new McpUnauthorizedError();
88
+ }
89
+ await authCache.verifyAndConsume({
90
+ wallet: walletLc,
91
+ nonce: nonce as Hex,
92
+ signature: sig as Hex,
93
+ });
94
+ return walletLc;
95
+ }
96
+
97
+ /**
98
+ * Direct JSON-RPC-ish dispatch — we expose the same RPC envelope MCP uses but
99
+ * over a synchronous request/response cycle (no SSE in v1). Clients send:
100
+ * { method: 'tools/list', params: {} }
101
+ * { method: 'tools/call', params: { name, arguments } }
102
+ */
103
+ async function dispatch(
104
+ ctx: McpServerContext,
105
+ body: unknown,
106
+ ): Promise<unknown> {
107
+ if (
108
+ typeof body !== 'object' ||
109
+ body === null ||
110
+ !('method' in body) ||
111
+ typeof (body as { method: unknown }).method !== 'string'
112
+ ) {
113
+ throw new McpInvalidArgsError();
114
+ }
115
+ const method = (body as { method: string }).method;
116
+ const params = ((body as { params?: unknown }).params ?? {}) as unknown;
117
+
118
+ if (method === 'tools/list') {
119
+ ListToolsRequestSchema.parse({ method, params });
120
+ return {
121
+ tools: ALL_TOOLS.map((t) => ({
122
+ name: t.name,
123
+ description: t.description,
124
+ inputSchema: t.inputSchema,
125
+ })),
126
+ };
127
+ }
128
+ if (method === 'tools/call') {
129
+ const parsed = CallToolRequestSchema.parse({ method, params });
130
+ const tool = findTool(parsed.params.name);
131
+ if (tool === undefined) {
132
+ throw new McpInvalidArgsError();
133
+ }
134
+ const args = parsed.params.arguments ?? {};
135
+ const result = await tool.handler(ctx, args);
136
+ return {
137
+ content: [{ type: 'text', text: JSON.stringify(result) }],
138
+ };
139
+ }
140
+ throw new McpInvalidArgsError();
141
+ }
142
+
143
+ export async function runHttpServer(opts: RunHttpServerOptions): Promise<{
144
+ readonly close: () => Promise<void>;
145
+ }> {
146
+ const bootstrap = opts.bootstrap ?? loadBootstrapConfigFromEnv();
147
+ const ctx = buildContextFromConfig(bootstrap);
148
+ const authCache = createAuthCache();
149
+ const allowList = bootstrap.httpAllowedWallets;
150
+
151
+ const server = createServer((req, res) => {
152
+ void handleRequest(req, res, ctx, authCache, allowList).catch((err) => {
153
+ if (err instanceof McpServerError) {
154
+ send(res, 400, { ok: false, code: err.code });
155
+ return;
156
+ }
157
+ send(res, 500, { ok: false, code: 'internal_error' });
158
+ });
159
+ });
160
+
161
+ await new Promise<void>((resolve) => {
162
+ server.listen(opts.port, () => resolve());
163
+ });
164
+
165
+ return {
166
+ close: () =>
167
+ new Promise<void>((resolve, reject) => {
168
+ server.close((err) => (err ? reject(err) : resolve()));
169
+ }),
170
+ };
171
+ }
172
+
173
+ async function handleRequest(
174
+ req: IncomingMessage,
175
+ res: ServerResponse,
176
+ ctx: McpServerContext,
177
+ authCache: AuthCache,
178
+ allowList: ReadonlySet<`0x${string}`>,
179
+ ): Promise<void> {
180
+ const url = req.url ?? '/';
181
+ const method = req.method ?? 'GET';
182
+
183
+ if (url === '/healthz' && method === 'GET') {
184
+ send(res, 200, { ok: true });
185
+ return;
186
+ }
187
+
188
+ if (url === '/auth/challenge' && method === 'POST') {
189
+ const body = (await readJsonBody(req)) as { wallet?: string };
190
+ if (
191
+ typeof body.wallet !== 'string' ||
192
+ !isHex(body.wallet) ||
193
+ body.wallet.length !== 42
194
+ ) {
195
+ throw new McpInvalidArgsError();
196
+ }
197
+ const challenge = authCache.issue(body.wallet.toLowerCase() as `0x${string}`);
198
+ send(res, 200, {
199
+ ok: true,
200
+ wallet: challenge.wallet,
201
+ nonce: challenge.nonce,
202
+ message: challenge.message,
203
+ expiresAtMs: challenge.expiresAtMs,
204
+ });
205
+ return;
206
+ }
207
+
208
+ if (url === '/mcp' && method === 'POST') {
209
+ await authenticate(req, authCache, allowList);
210
+ const body = await readJsonBody(req);
211
+ const result = await dispatch(ctx, body);
212
+ send(res, 200, { ok: true, result });
213
+ return;
214
+ }
215
+
216
+ send(res, 404, { ok: false, code: 'not_found' });
217
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Public barrel — @shroud-fi/mcp-server.
3
+ *
4
+ * Two consumption paths:
5
+ * - `import { runStdioServer } from '@shroud-fi/mcp-server'` for stdio bin.
6
+ * - `import { runHttpServer } from '@shroud-fi/mcp-server/http'` for HTTP.
7
+ * - `import { ALL_TOOLS } from '@shroud-fi/mcp-server/tools'` to embed the
8
+ * tool registry into a host MCP server you already own.
9
+ */
10
+
11
+ export { runStdioServer } from './stdio.js';
12
+ export { runHttpServer } from './http.js';
13
+ export { buildMcpServer } from './server.js';
14
+ export {
15
+ buildContextFromConfig,
16
+ loadBootstrapConfigFromEnv,
17
+ readHttpPortFromEnv,
18
+ } from './config.js';
19
+ export { createAuthCache } from './auth.js';
20
+ export { ALL_TOOLS, findTool } from './tools/index.js';
21
+ export type {
22
+ McpTool,
23
+ McpToolResult,
24
+ McpServerContext,
25
+ McpServerBootstrapConfig,
26
+ Eip191Challenge,
27
+ } from './types.js';
28
+ export {
29
+ McpServerError,
30
+ McpConfigError,
31
+ McpUnauthorizedError,
32
+ McpToolNotFoundError,
33
+ McpInvalidArgsError,
34
+ McpExecutionError,
35
+ McpEip191ChallengeExpiredError,
36
+ McpEip191SignatureInvalidError,
37
+ } from './errors.js';
38
+ export {
39
+ SHROUDFI_MCP_SERVER_NAME,
40
+ SHROUDFI_MCP_SERVER_VERSION,
41
+ } from './constants.js';
package/src/server.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * MCP server core — wires our tool registry into the official MCP SDK.
3
+ *
4
+ * Transport-agnostic: this function builds the configured Server instance.
5
+ * stdio.ts and http.ts each connect it to their respective transport.
6
+ */
7
+
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import {
10
+ CallToolRequestSchema,
11
+ ListToolsRequestSchema,
12
+ type Tool as MCPTool,
13
+ } from '@modelcontextprotocol/sdk/types.js';
14
+ import { ALL_TOOLS, findTool } from './tools/index.js';
15
+ import {
16
+ SHROUDFI_MCP_SERVER_NAME,
17
+ SHROUDFI_MCP_SERVER_VERSION,
18
+ } from './constants.js';
19
+ import { McpInvalidArgsError, McpToolNotFoundError, McpServerError } from './errors.js';
20
+ import type { McpServerContext } from './types.js';
21
+
22
+ /**
23
+ * Build a configured MCP Server. The caller plugs in a transport via
24
+ * `server.connect(transport)` once this returns.
25
+ */
26
+ export function buildMcpServer(ctx: McpServerContext): Server {
27
+ const server = new Server(
28
+ {
29
+ name: SHROUDFI_MCP_SERVER_NAME,
30
+ version: SHROUDFI_MCP_SERVER_VERSION,
31
+ },
32
+ {
33
+ capabilities: {
34
+ tools: {},
35
+ },
36
+ },
37
+ );
38
+
39
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
40
+ const tools: MCPTool[] = ALL_TOOLS.map((t) => ({
41
+ name: t.name,
42
+ description: t.description,
43
+ inputSchema: t.inputSchema as MCPTool['inputSchema'],
44
+ }));
45
+ return { tools };
46
+ });
47
+
48
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
49
+ const name = req.params.name;
50
+ const tool = findTool(name);
51
+ if (tool === undefined) {
52
+ throw new McpToolNotFoundError();
53
+ }
54
+ try {
55
+ const args = (req.params.arguments ?? {}) as unknown;
56
+ const result = await tool.handler(ctx, args);
57
+ return {
58
+ content: [
59
+ {
60
+ type: 'text',
61
+ text: JSON.stringify(result),
62
+ },
63
+ ],
64
+ };
65
+ } catch (err) {
66
+ if (err instanceof McpInvalidArgsError || err instanceof McpToolNotFoundError) {
67
+ return {
68
+ isError: true,
69
+ content: [
70
+ {
71
+ type: 'text',
72
+ text: JSON.stringify({ ok: false, code: err.code, message: err.message }),
73
+ },
74
+ ],
75
+ };
76
+ }
77
+ if (err instanceof McpServerError) {
78
+ return {
79
+ isError: true,
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: JSON.stringify({ ok: false, code: err.code, message: err.message }),
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ // Wrap unexpected error — short tag only, never the raw .message.
89
+ const code = (err as { name?: string })?.name ?? 'execution_failed';
90
+ return {
91
+ isError: true,
92
+ content: [
93
+ {
94
+ type: 'text',
95
+ text: JSON.stringify({ ok: false, code, message: 'Tool execution failed' }),
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ });
101
+
102
+ return server;
103
+ }
package/src/stdio.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Stdio transport entry point.
3
+ *
4
+ * Reads operator config from env, builds the agent context, attaches the
5
+ * MCP server to stdin/stdout. Used by `.mcp.json` config in Claude Code,
6
+ * Cursor, Windsurf, Zed AI, etc.
7
+ */
8
+
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { buildContextFromConfig, loadBootstrapConfigFromEnv } from './config.js';
11
+ import { buildMcpServer } from './server.js';
12
+
13
+ export async function runStdioServer(): Promise<void> {
14
+ const bootstrap = loadBootstrapConfigFromEnv();
15
+ const ctx = buildContextFromConfig(bootstrap);
16
+ const server = buildMcpServer(ctx);
17
+ const transport = new StdioServerTransport();
18
+ await server.connect(transport);
19
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * shroud_balance — read ETH and (optionally) ERC-20 balance for a wallet.
3
+ *
4
+ * Defaults to the agent's operator wallet. Pass `address` to query a different
5
+ * one (e.g. a stealth address whose private key was just recovered).
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { ERC20Abi } from '@shroud-fi/transport';
10
+ import type { McpTool, McpToolResult } from '../types.js';
11
+ import { McpInvalidArgsError } from '../errors.js';
12
+ import { AddressSchema } from './schema.js';
13
+
14
+ const ArgsSchema = z
15
+ .object({
16
+ address: AddressSchema.optional(),
17
+ token: AddressSchema.optional(),
18
+ })
19
+ .strict();
20
+
21
+ export const shroudBalanceTool: McpTool = {
22
+ name: 'shroud_balance',
23
+ description:
24
+ 'Read the ETH balance and (optionally) a single ERC-20 balance for a wallet. Defaults to the operator wallet. Returns wei-denominated strings.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ additionalProperties: false,
28
+ properties: {
29
+ address: { type: 'string', pattern: '^0x[0-9a-fA-F]{40}$' },
30
+ token: { type: 'string', pattern: '^0x[0-9a-fA-F]{40}$' },
31
+ },
32
+ },
33
+ async handler(ctx, raw): Promise<McpToolResult> {
34
+ const parsed = ArgsSchema.safeParse(raw ?? {});
35
+ if (!parsed.success) {
36
+ throw new McpInvalidArgsError();
37
+ }
38
+ const target = (parsed.data.address ?? ctx.walletAddress) as
39
+ | `0x${string}`
40
+ | undefined;
41
+ if (target === undefined) {
42
+ throw new McpInvalidArgsError();
43
+ }
44
+
45
+ const ethBal = await ctx.transport.publicClient.getBalance({
46
+ address: target,
47
+ });
48
+
49
+ const out: Record<string, unknown> = {
50
+ ok: true,
51
+ address: target,
52
+ ethWei: ethBal.toString(),
53
+ };
54
+ if (parsed.data.token !== undefined) {
55
+ const tokenBal = (await ctx.transport.publicClient.readContract({
56
+ address: parsed.data.token as `0x${string}`,
57
+ abi: ERC20Abi,
58
+ functionName: 'balanceOf',
59
+ args: [target],
60
+ })) as bigint;
61
+ out.token = parsed.data.token;
62
+ out.tokenBaseUnits = tokenBal.toString();
63
+ }
64
+ return out as McpToolResult;
65
+ },
66
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tool registry. Add new tools here so both stdio + http transports pick them up.
3
+ */
4
+
5
+ import type { McpTool } from '../types.js';
6
+ import { shroudRegisterTool } from './register.js';
7
+ import { shroudSendTool } from './send.js';
8
+ import { shroudSendToWalletTool } from './send-to-wallet.js';
9
+ import { shroudReceiveTool } from './receive.js';
10
+ import { shroudSweepTool } from './sweep.js';
11
+ import { shroudBalanceTool } from './balance.js';
12
+ import { shroudStatusTool } from './status.js';
13
+ import { shroudX402PayTool } from './x402-pay.js';
14
+ import { shroudX402ServeTool } from './x402-serve.js';
15
+
16
+ export const ALL_TOOLS: readonly McpTool[] = [
17
+ shroudRegisterTool,
18
+ shroudSendTool,
19
+ shroudSendToWalletTool,
20
+ shroudReceiveTool,
21
+ shroudSweepTool,
22
+ shroudBalanceTool,
23
+ shroudStatusTool,
24
+ shroudX402PayTool,
25
+ shroudX402ServeTool,
26
+ ];
27
+
28
+ export function findTool(name: string): McpTool | undefined {
29
+ return ALL_TOOLS.find((t) => t.name === name);
30
+ }
31
+
32
+ export {
33
+ shroudRegisterTool,
34
+ shroudSendTool,
35
+ shroudSendToWalletTool,
36
+ shroudReceiveTool,
37
+ shroudSweepTool,
38
+ shroudBalanceTool,
39
+ shroudStatusTool,
40
+ shroudX402PayTool,
41
+ shroudX402ServeTool,
42
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * shroud_receive — scan a block range for incoming stealth payments to the
3
+ * configured agent. Stateless: caller supplies fromBlock + toBlock; server
4
+ * returns every detection in that window.
5
+ *
6
+ * Privacy: stealthPrivateKey is exposed in the response because the caller is
7
+ * the same operator that holds the spending key in the server process — the
8
+ * key was derived from their own seed. The MCP transport must be local stdio
9
+ * or authenticated HTTP (EIP-191) to keep this safe. See HTTP transport gate.
10
+ */
11
+
12
+ import { z } from 'zod';
13
+ import { createScanner } from '@shroud-fi/scanning';
14
+ import type { DetectedPayment } from '@shroud-fi/scanning';
15
+ import type { McpTool, McpToolResult } from '../types.js';
16
+ import { McpInvalidArgsError } from '../errors.js';
17
+ import { Uint256StringSchema, toBigInt } from './schema.js';
18
+
19
+ const ArgsSchema = z
20
+ .object({
21
+ fromBlock: Uint256StringSchema,
22
+ toBlock: z.union([Uint256StringSchema, z.literal('latest')]).optional(),
23
+ finality: z.enum(['unsafe', 'safe', 'finalized']).optional(),
24
+ /** Hard cap on detections returned in a single call. Defaults to 100. */
25
+ limit: z.number().int().positive().max(1000).optional(),
26
+ })
27
+ .strict();
28
+
29
+ export const shroudReceiveTool: McpTool = {
30
+ name: 'shroud_receive',
31
+ description:
32
+ 'Scan a block range for incoming stealth payments to the agent. Returns the list of detections (each includes stealthAddress, ephemeralPubKey, stealthPrivateKey, txHash, blockNumber). Stateless: caller drives the cursor by passing fromBlock + toBlock.',
33
+ inputSchema: {
34
+ type: 'object',
35
+ additionalProperties: false,
36
+ properties: {
37
+ fromBlock: { type: 'string', pattern: '^\\d+$' },
38
+ toBlock: { oneOf: [{ type: 'string', pattern: '^\\d+$' }, { const: 'latest' }] },
39
+ finality: { type: 'string', enum: ['unsafe', 'safe', 'finalized'] },
40
+ limit: { type: 'integer', minimum: 1, maximum: 1000 },
41
+ },
42
+ required: ['fromBlock'],
43
+ },
44
+ async handler(ctx, raw): Promise<McpToolResult> {
45
+ const parsed = ArgsSchema.safeParse(raw);
46
+ if (!parsed.success) {
47
+ throw new McpInvalidArgsError();
48
+ }
49
+ const { fromBlock, toBlock, finality, limit } = parsed.data;
50
+ const fromB = toBigInt(fromBlock);
51
+
52
+ let toB: bigint;
53
+ if (toBlock === undefined || toBlock === 'latest') {
54
+ const tag: 'finalized' | 'safe' | 'latest' =
55
+ finality === 'finalized'
56
+ ? 'finalized'
57
+ : finality === 'safe'
58
+ ? 'safe'
59
+ : 'latest';
60
+ const block = await ctx.transport.publicClient.getBlock({ blockTag: tag });
61
+ toB = block.number ?? fromB;
62
+ } else {
63
+ toB = toBigInt(toBlock);
64
+ }
65
+
66
+ if (toB < fromB) {
67
+ return { ok: true, fromBlock: fromB.toString(), toBlock: toB.toString(), detections: [] };
68
+ }
69
+
70
+ const scanner = createScanner({
71
+ transport: ctx.transport,
72
+ scanningKey: ctx.agent.identity.keys.scanningKey,
73
+ spendingKey: ctx.agent.identity.keys.spendingKey,
74
+ startBlock: fromB,
75
+ ...(finality !== undefined ? { finality } : {}),
76
+ });
77
+
78
+ const out: Array<Record<string, unknown>> = [];
79
+ const cap = limit ?? 100;
80
+ try {
81
+ for await (const d of scanner.scanRange(fromB, toB) as AsyncIterable<DetectedPayment>) {
82
+ out.push({
83
+ stealthAddress: d.stealthAddress,
84
+ ephemeralPubKey: d.ephemeralPubKey,
85
+ stealthPrivateKey: d.stealthPrivateKey,
86
+ txHash: d.txHash,
87
+ blockNumber: d.blockNumber.toString(),
88
+ blockHash: d.blockHash,
89
+ logIndex: d.logIndex,
90
+ finality: d.finality,
91
+ });
92
+ if (out.length >= cap) break;
93
+ }
94
+ } finally {
95
+ scanner.close();
96
+ }
97
+
98
+ return {
99
+ ok: true,
100
+ fromBlock: fromB.toString(),
101
+ toBlock: toB.toString(),
102
+ detections: out,
103
+ };
104
+ },
105
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * shroud_register — publish the agent's stealth meta-address into ERC-6538.
3
+ *
4
+ * Idempotent. Returns `{ registered: false }` if the wallet already has an
5
+ * on-chain entry.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import type { McpTool, McpToolResult } from '../types.js';
10
+ import { McpInvalidArgsError } from '../errors.js';
11
+
12
+ const ArgsSchema = z.object({}).strict();
13
+
14
+ export const shroudRegisterTool: McpTool = {
15
+ name: 'shroud_register',
16
+ description:
17
+ 'Publish the agent stealth meta-address into ERC-6538. Idempotent — no-op when already registered. Returns the registration tx hash on first call.',
18
+ inputSchema: {
19
+ type: 'object',
20
+ additionalProperties: false,
21
+ properties: {},
22
+ required: [],
23
+ },
24
+ async handler(ctx, raw): Promise<McpToolResult> {
25
+ const parsed = ArgsSchema.safeParse(raw ?? {});
26
+ if (!parsed.success) {
27
+ throw new McpInvalidArgsError();
28
+ }
29
+ const result = await ctx.agent.ensureRegistered();
30
+ return {
31
+ ok: true,
32
+ registered: result.registered,
33
+ ...(result.txHash !== undefined ? { txHash: result.txHash } : {}),
34
+ metaAddress: ctx.agent.metaAddressEncoded,
35
+ };
36
+ },
37
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared zod schemas + helpers for tool argument validation.
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** 0x-prefixed lowercase EVM address (case preserved by viem checks). */
8
+ export const AddressSchema = z
9
+ .string()
10
+ .regex(/^0x[0-9a-fA-F]{40}$/, '0x-prefixed 20-byte hex required');
11
+
12
+ /** Asset descriptor used by send tools: 'ETH' or token address. */
13
+ export const AssetSchema = z.union([
14
+ z.literal('ETH'),
15
+ z.object({ token: AddressSchema }),
16
+ ]);
17
+
18
+ /** Positive integer string that parses to bigint. */
19
+ export const Uint256StringSchema = z
20
+ .string()
21
+ .regex(/^\d+$/, 'positive integer required');
22
+
23
+ export function toBigInt(v: string): bigint {
24
+ return BigInt(v);
25
+ }