@m2msentinel/sdk 1.1.1 → 1.2.0

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/mcp_server.js CHANGED
@@ -1,223 +1,492 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const https = require('https');
5
- const http = require('http');
6
- const readline = require('readline');
7
-
8
- const BASE_URL = process.env.M2M_SENTINEL_BASE_URL || 'https://api.m2msentinel.com';
9
- const API_KEY = process.env.M2M_SENTINEL_API_KEY || '';
10
- const TIMEOUT_MS = Number(process.env.M2M_SENTINEL_TIMEOUT_MS || 30000);
11
-
12
- const TOOLS = [
13
- {
14
- name: 'm2m_audit_contract',
15
- description: 'Return selected static bytecode capability observations, common proxy resolution, limitations, and provenance for a Base contract. This is factual capability observation, not a safety or exploitability guarantee.',
16
- inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Base contract address (0x...)' } }, required: ['address'] }
17
- },
18
- {
19
- name: 'm2m_get_gas_metrics',
20
- description: 'Return sourced Base gas fee metrics and execution recommendations.',
21
- inputSchema: { type: 'object', properties: {} }
22
- },
23
- {
24
- name: 'm2m_get_token_price',
25
- description: 'Return sourced Base DEX token price observation for allowlisted assets (e.g. USDC, WETH).',
26
- inputSchema: { type: 'object', properties: { symbol: { type: 'string', description: 'Token symbol (USDC, WETH)' } }, required: ['symbol'] }
27
- },
28
- {
29
- name: 'm2m_get_dex_liquidity',
30
- description: 'Return tracked Base DEX pool reserve and liquidity metrics.',
31
- inputSchema: { type: 'object', properties: { pair: { type: 'string' } } }
32
- },
33
- {
34
- name: 'm2m_get_whale_signals',
35
- description: 'Return tracked Base whale transfer signals.',
36
- inputSchema: { type: 'object', properties: { limit: { type: 'number' } } }
37
- },
38
- {
39
- name: 'm2m_get_service_status',
40
- description: 'Return real-time operational status of M2M Sentinel upstream RPC and persistence rails.',
41
- inputSchema: { type: 'object', properties: {} }
42
- },
43
- // Backwards compatibility aliases
44
- {
45
- name: 'audit_contract',
46
- description: 'Alias for m2m_audit_contract.',
47
- inputSchema: { type: 'object', properties: { address: { type: 'string' } }, required: ['address'] }
48
- },
49
- {
50
- name: 'get_capability_score',
51
- description: 'Return the static capability coverage index and provenance for a Base contract. This is not a safety score.',
52
- inputSchema: { type: 'object', properties: { address: { type: 'string' } }, required: ['address'] }
53
- },
54
- {
55
- name: 'get_gas_fees',
56
- description: 'Alias for m2m_get_gas_metrics.',
57
- inputSchema: { type: 'object', properties: {} }
58
- },
59
- {
60
- name: 'get_dex_metrics',
61
- description: 'Alias for m2m_get_dex_liquidity.',
62
- inputSchema: { type: 'object', properties: {} }
63
- },
64
- {
65
- name: 'get_token_price',
66
- description: 'Alias for m2m_get_token_price.',
67
- inputSchema: { type: 'object', properties: { symbol: { type: 'string' } }, required: ['symbol'] }
68
- },
69
- {
70
- name: 'get_whale_signals',
71
- description: 'Alias for m2m_get_whale_signals.',
72
- inputSchema: { type: 'object', properties: {} }
73
- }
74
- ];
75
-
76
- function parseX402Header(value) {
77
- if (!value) return null;
78
- try { return JSON.parse(value); } catch (_) {}
79
- try {
80
- const normalized = String(value).replace(/-/g, '+').replace(/_/g, '/');
81
- const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
82
- return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
83
- } catch (_) {
84
- return null;
85
- }
86
- }
87
-
88
- function queryApi(path) {
89
- return new Promise((resolve, reject) => {
90
- const url = new URL(path, BASE_URL);
91
- const isHttps = url.protocol === 'https:';
92
- const transport = isHttps ? https : http;
93
- const headers = { Accept: 'application/json', 'User-Agent': 'M2MSentinel-MCP/1.1.0' };
94
- if (API_KEY) headers['x-api-key'] = API_KEY;
95
-
96
- const req = transport.request(url, { method: 'GET', headers, timeout: TIMEOUT_MS }, (res) => {
97
- let data = '';
98
- res.setEncoding('utf8');
99
- res.on('data', (chunk) => { data += chunk; });
100
- res.on('end', () => {
101
- let body = null;
102
- if (data) {
103
- try { body = JSON.parse(data); } catch (_) { body = { raw: data }; }
104
- }
105
- resolve({
106
- status: res.statusCode,
107
- ok: res.statusCode >= 200 && res.statusCode < 300,
108
- body,
109
- paymentRequired: parseX402Header(res.headers['payment-required'] || res.headers['x-payment-required']),
110
- paymentResponse: parseX402Header(res.headers['payment-response'] || res.headers['x-payment-response']),
111
- retryAfter: res.headers['retry-after'] || null
112
- });
113
- });
114
- });
115
- req.on('timeout', () => req.destroy(new Error('M2M Sentinel request timed out')));
116
- req.on('error', reject);
117
- req.end();
118
- });
119
- }
120
-
121
- function pathForTool(name, args) {
122
- const input = args || {};
123
- switch (name) {
124
- case 'm2m_audit_contract':
125
- case 'audit_contract':
126
- if (!input.address) throw new Error('address is required');
127
- return '/v1/audit/' + encodeURIComponent(input.address);
128
- case 'get_capability_score':
129
- if (!input.address) throw new Error('address is required');
130
- return '/v1/security/score/' + encodeURIComponent(input.address);
131
- case 'm2m_get_gas_metrics':
132
- case 'get_gas_fees':
133
- return '/v1/gas/fees';
134
- case 'm2m_get_dex_liquidity':
135
- case 'get_dex_metrics':
136
- return '/v1/dex/metrics';
137
- case 'm2m_get_token_price':
138
- case 'get_token_price':
139
- if (!input.symbol) throw new Error('symbol is required');
140
- return '/v1/token/price/' + encodeURIComponent(input.symbol);
141
- case 'm2m_get_whale_signals':
142
- case 'get_whale_signals':
143
- return '/v1/whales/signals';
144
- case 'm2m_get_service_status':
145
- return '/v1/status';
146
- default:
147
- throw new Error('Unknown tool: ' + name);
148
- }
149
- }
150
-
151
- function send(message) {
152
- process.stdout.write(JSON.stringify(message) + '\n');
153
- }
154
-
155
- function success(id, result) {
156
- send({ jsonrpc: '2.0', id, result });
157
- }
158
-
159
- function failure(id, code, message, data) {
160
- const error = { code, message };
161
- if (data !== undefined) error.data = data;
162
- send({ jsonrpc: '2.0', id, error });
163
- }
164
-
165
- async function handle(request) {
166
- if (!request || request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
167
- failure(request && request.id !== undefined ? request.id : null, -32600, 'Invalid Request');
168
- return;
169
- }
170
- if (request.id === undefined || request.id === null) return;
171
-
172
- try {
173
- if (request.method === 'initialize') {
174
- success(request.id, {
175
- protocolVersion: request.params && request.params.protocolVersion ? request.params.protocolVersion : '2024-11-05',
176
- capabilities: { tools: {} },
177
- serverInfo: { name: 'm2m-sentinel-mcp', version: '1.1.0' }
178
- });
179
- return;
180
- }
181
- if (request.method === 'tools/list') {
182
- success(request.id, { tools: TOOLS });
183
- return;
184
- }
185
- if (request.method === 'tools/call') {
186
- const params = request.params || {};
187
- const path = pathForTool(params.name, params.arguments || {});
188
- const apiResult = await queryApi(path);
189
- const isError = !apiResult.ok;
190
- success(request.id, {
191
- content: [{ type: 'text', text: typeof apiResult.body === 'object' ? JSON.stringify(apiResult.body, null, 2) : String(apiResult.body) }],
192
- isError,
193
- structuredContent: apiResult.body,
194
- _meta: {
195
- httpStatus: apiResult.status,
196
- paymentRequired: apiResult.paymentRequired,
197
- paymentResponse: apiResult.paymentResponse,
198
- retryAfter: apiResult.retryAfter,
199
- notASafetyGuarantee: true
200
- }
201
- });
202
- return;
203
- }
204
- failure(request.id, -32601, 'Method not found');
205
- } catch (err) {
206
- failure(request.id, -32603, err && err.message ? err.message : 'Internal error');
207
- }
208
- }
209
-
210
- const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
211
- rl.on('line', (line) => {
212
- if (!line.trim()) return;
213
- let request;
214
- try {
215
- request = JSON.parse(line);
216
- } catch (_) {
217
- failure(null, -32700, 'Parse error');
218
- return;
219
- }
220
- handle(request);
221
- });
222
-
223
- module.exports = { TOOLS, pathForTool, parseX402Header };
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const https = require('https');
5
+ const http = require('http');
6
+ const readline = require('readline');
7
+
8
+ let BASE_URL = process.env.M2M_SENTINEL_BASE_URL || 'https://api.m2msentinel.com';
9
+ let API_KEY = process.env.M2M_SENTINEL_API_KEY || '';
10
+ const TIMEOUT_MS = Number(process.env.M2M_SENTINEL_TIMEOUT_MS || 30000);
11
+ const VERSION = '1.2.0';
12
+
13
+ const TOOLS = [
14
+ {
15
+ name: 'm2m_audit_contract',
16
+ description: 'Return selected static bytecode capability observations, common proxy resolution, limitations, and provenance for a Base contract. This is factual capability observation, not a safety or exploitability guarantee.',
17
+ inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Base contract address (0x...)' } }, required: ['address'] }
18
+ },
19
+ {
20
+ name: 'm2m_get_gas_metrics',
21
+ description: 'Return sourced Base gas fee metrics and execution recommendations.',
22
+ inputSchema: { type: 'object', properties: {} }
23
+ },
24
+ {
25
+ name: 'm2m_get_token_price',
26
+ description: 'Return sourced Base DEX token price observation for allowlisted assets (e.g. USDC, WETH).',
27
+ inputSchema: { type: 'object', properties: { symbol: { type: 'string', description: 'Token symbol (USDC, WETH)' } }, required: ['symbol'] }
28
+ },
29
+ {
30
+ name: 'm2m_get_dex_liquidity',
31
+ description: 'Return tracked Base DEX pool reserve and liquidity metrics.',
32
+ inputSchema: { type: 'object', properties: { pair: { type: 'string' } } }
33
+ },
34
+ {
35
+ name: 'm2m_get_whale_signals',
36
+ description: 'Return tracked Base whale transfer signals.',
37
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' } } }
38
+ },
39
+ {
40
+ name: 'm2m_get_service_status',
41
+ description: 'Return real-time operational status of M2M Sentinel upstream RPC and persistence rails.',
42
+ inputSchema: { type: 'object', properties: {} }
43
+ },
44
+ // Backwards compatibility aliases
45
+ {
46
+ name: 'audit_contract',
47
+ description: 'Alias for m2m_audit_contract.',
48
+ inputSchema: { type: 'object', properties: { address: { type: 'string' } }, required: ['address'] }
49
+ },
50
+ {
51
+ name: 'get_capability_score',
52
+ description: 'Return the static capability coverage index and provenance for a Base contract. This is not a safety score.',
53
+ inputSchema: { type: 'object', properties: { address: { type: 'string' } }, required: ['address'] }
54
+ },
55
+ {
56
+ name: 'get_gas_fees',
57
+ description: 'Alias for m2m_get_gas_metrics.',
58
+ inputSchema: { type: 'object', properties: {} }
59
+ },
60
+ {
61
+ name: 'get_dex_metrics',
62
+ description: 'Alias for m2m_get_dex_liquidity.',
63
+ inputSchema: { type: 'object', properties: {} }
64
+ },
65
+ {
66
+ name: 'get_token_price',
67
+ description: 'Alias for m2m_get_token_price.',
68
+ inputSchema: { type: 'object', properties: { symbol: { type: 'string' } }, required: ['symbol'] }
69
+ },
70
+ {
71
+ name: 'get_whale_signals',
72
+ description: 'Alias for m2m_get_whale_signals.',
73
+ inputSchema: { type: 'object', properties: {} }
74
+ }
75
+ ];
76
+
77
+ function parseX402Header(value) {
78
+ if (!value) return null;
79
+ try { return JSON.parse(value); } catch (_) {}
80
+ try {
81
+ const normalized = String(value).replace(/-/g, '+').replace(/_/g, '/');
82
+ const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
83
+ return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
84
+ } catch (_) {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function queryApi(path, customApiKey = null, customBaseUrl = null) {
90
+ const activeBaseUrl = customBaseUrl || BASE_URL;
91
+ const activeApiKey = customApiKey !== null ? customApiKey : API_KEY;
92
+
93
+ return new Promise((resolve, reject) => {
94
+ const url = new URL(path, activeBaseUrl);
95
+ const isHttps = url.protocol === 'https:';
96
+ const transport = isHttps ? https : http;
97
+ const headers = { Accept: 'application/json', 'User-Agent': `M2MSentinel-CLI/${VERSION}` };
98
+ if (activeApiKey) headers['x-api-key'] = activeApiKey;
99
+
100
+ const start = Date.now();
101
+ const req = transport.request(url, { method: 'GET', headers, timeout: TIMEOUT_MS }, (res) => {
102
+ let data = '';
103
+ res.setEncoding('utf8');
104
+ res.on('data', (chunk) => { data += chunk; });
105
+ res.on('end', () => {
106
+ const latencyMs = Date.now() - start;
107
+ let body = null;
108
+ if (data) {
109
+ try { body = JSON.parse(data); } catch (_) { body = { raw: data }; }
110
+ }
111
+ resolve({
112
+ status: res.statusCode,
113
+ ok: res.statusCode >= 200 && res.statusCode < 300,
114
+ body,
115
+ latencyMs,
116
+ paymentRequired: parseX402Header(res.headers['payment-required'] || res.headers['x-payment-required']),
117
+ paymentResponse: parseX402Header(res.headers['payment-response'] || res.headers['x-payment-response']),
118
+ retryAfter: res.headers['retry-after'] || null
119
+ });
120
+ });
121
+ });
122
+ req.on('timeout', () => req.destroy(new Error('M2M Sentinel request timed out')));
123
+ req.on('error', reject);
124
+ req.end();
125
+ });
126
+ }
127
+
128
+ function pathForTool(name, args) {
129
+ const input = args || {};
130
+ switch (name) {
131
+ case 'm2m_audit_contract':
132
+ case 'audit_contract':
133
+ if (!input.address) throw new Error('address is required');
134
+ return '/v1/audit/' + encodeURIComponent(input.address);
135
+ case 'get_capability_score':
136
+ if (!input.address) throw new Error('address is required');
137
+ return '/v1/security/score/' + encodeURIComponent(input.address);
138
+ case 'm2m_get_gas_metrics':
139
+ case 'get_gas_fees':
140
+ return '/v1/gas/fees';
141
+ case 'm2m_get_dex_liquidity':
142
+ case 'get_dex_metrics':
143
+ return '/v1/dex/metrics';
144
+ case 'm2m_get_token_price':
145
+ case 'get_token_price':
146
+ if (!input.symbol) throw new Error('symbol is required');
147
+ return '/v1/token/price/' + encodeURIComponent(input.symbol);
148
+ case 'm2m_get_whale_signals':
149
+ case 'get_whale_signals':
150
+ return '/v1/whales/signals';
151
+ case 'm2m_get_service_status':
152
+ return '/v1/status';
153
+ default:
154
+ throw new Error('Unknown tool: ' + name);
155
+ }
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Model Context Protocol (MCP) JSON-RPC Handlers
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function send(message) {
163
+ process.stdout.write(JSON.stringify(message) + '\n');
164
+ }
165
+
166
+ function success(id, result) {
167
+ send({ jsonrpc: '2.0', id, result });
168
+ }
169
+
170
+ function failure(id, code, message, data) {
171
+ const error = { code, message };
172
+ if (data !== undefined) error.data = data;
173
+ send({ jsonrpc: '2.0', id, error });
174
+ }
175
+
176
+ async function handleMcpMessage(request) {
177
+ if (!request || request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
178
+ failure(request && request.id !== undefined ? request.id : null, -32600, 'Invalid Request');
179
+ return;
180
+ }
181
+ if (request.id === undefined || request.id === null) return;
182
+
183
+ try {
184
+ if (request.method === 'initialize') {
185
+ success(request.id, {
186
+ protocolVersion: request.params && request.params.protocolVersion ? request.params.protocolVersion : '2024-11-05',
187
+ capabilities: { tools: {} },
188
+ serverInfo: { name: 'm2m-sentinel-mcp', version: VERSION }
189
+ });
190
+ return;
191
+ }
192
+ if (request.method === 'tools/list') {
193
+ success(request.id, { tools: TOOLS });
194
+ return;
195
+ }
196
+ if (request.method === 'tools/call') {
197
+ const params = request.params || {};
198
+ const path = pathForTool(params.name, params.arguments || {});
199
+ const apiResult = await queryApi(path);
200
+ const isError = !apiResult.ok;
201
+ success(request.id, {
202
+ content: [{ type: 'text', text: typeof apiResult.body === 'object' ? JSON.stringify(apiResult.body, null, 2) : String(apiResult.body) }],
203
+ isError,
204
+ structuredContent: apiResult.body,
205
+ _meta: {
206
+ httpStatus: apiResult.status,
207
+ paymentRequired: apiResult.paymentRequired,
208
+ paymentResponse: apiResult.paymentResponse,
209
+ retryAfter: apiResult.retryAfter,
210
+ notASafetyGuarantee: true
211
+ }
212
+ });
213
+ return;
214
+ }
215
+ failure(request.id, -32601, 'Method not found');
216
+ } catch (err) {
217
+ failure(request.id, -32603, err && err.message ? err.message : 'Internal error');
218
+ }
219
+ }
220
+
221
+ function startMcpServer() {
222
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
223
+ rl.on('line', (line) => {
224
+ if (!line.trim()) return;
225
+ let request;
226
+ try {
227
+ request = JSON.parse(line);
228
+ } catch (_) {
229
+ failure(null, -32700, 'Parse error');
230
+ return;
231
+ }
232
+ handleMcpMessage(request);
233
+ });
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Interactive Human Developer CLI
238
+ // ---------------------------------------------------------------------------
239
+
240
+ function printHelp() {
241
+ console.log(`
242
+ ================================================================
243
+ šŸ›”ļø M2M SENTINEL CLI — Base Bytecode & Preflight Intelligence
244
+ Version: ${VERSION} | Gateway: ${BASE_URL}
245
+ ================================================================
246
+
247
+ USAGE:
248
+ npx @m2msentinel/sdk <command> [arguments] [flags]
249
+ npx m2m-sentinel <command> [arguments] [flags]
250
+
251
+ COMMANDS:
252
+ audit <address> Disassemble bytecode, detect capabilities & resolve proxies
253
+ gas Fetch real-time Base Mainnet gas metrics & advice
254
+ price <symbol> Fetch DEX liquidity-weighted token price (USDC, WETH, AERO)
255
+ dex [pair] Fetch tracked Base liquidity pool metrics
256
+ whales [limit] Fetch large ERC-20 transfer signals on Base
257
+ status Check live API, RPC quorum, and persistence health
258
+ mcp Launch Model Context Protocol stdio JSON-RPC server
259
+
260
+ FLAGS:
261
+ --json Output raw JSON (ideal for scripts & jq)
262
+ --api-key <key> Authenticate with a paid or custom API key
263
+ --base-url <url> Override gateway endpoint (default: https://api.m2msentinel.com)
264
+ --help, -h Display this help manual
265
+ --version, -v Show installed CLI version
266
+
267
+ EXAMPLES:
268
+ # Inspect Base USDC proxy implementation and capabilities:
269
+ npx @m2msentinel/sdk audit 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
270
+
271
+ # Shorthand audit with direct address:
272
+ npx @m2msentinel/sdk 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
273
+
274
+ # Check Base gas fees as structured JSON:
275
+ npx @m2msentinel/sdk gas --json
276
+
277
+ # Connect to Claude Desktop / Cursor via MCP:
278
+ npx @m2msentinel/sdk mcp
279
+
280
+ Documentation: https://m2msentinel.com/docs.html
281
+ ================================================================
282
+ `);
283
+ }
284
+
285
+ async function runCli(args) {
286
+ let isJson = false;
287
+ let customApiKey = null;
288
+ let customBaseUrl = null;
289
+ const positional = [];
290
+
291
+ for (let i = 0; i < args.length; i++) {
292
+ const arg = args[i];
293
+ if (arg === '--json') {
294
+ isJson = true;
295
+ } else if (arg === '--api-key' && args[i + 1]) {
296
+ customApiKey = args[++i];
297
+ } else if (arg === '--base-url' && args[i + 1]) {
298
+ customBaseUrl = args[++i];
299
+ } else if (arg === '--help' || arg === '-h' || arg === 'help') {
300
+ printHelp();
301
+ return 0;
302
+ } else if (arg === '--version' || arg === '-v' || arg === 'version') {
303
+ console.log(`@m2msentinel/sdk v${VERSION}`);
304
+ return 0;
305
+ } else {
306
+ positional.push(arg);
307
+ }
308
+ }
309
+
310
+ const cmd = positional[0] || 'help';
311
+
312
+ // Handle explicit 'mcp' command
313
+ if (cmd === 'mcp') {
314
+ startMcpServer();
315
+ return 0;
316
+ }
317
+
318
+ // Handle address shorthand: npx @m2msentinel/sdk 0x8335...
319
+ let targetPath = null;
320
+ let commandName = cmd;
321
+
322
+ if (/^0x[a-fA-F0-9]{40}$/.test(cmd)) {
323
+ targetPath = `/v1/audit/${cmd}`;
324
+ commandName = 'audit';
325
+ } else if (cmd === 'audit') {
326
+ const addr = positional[1];
327
+ if (!addr || !/^0x[a-fA-F0-9]{40}$/.test(addr)) {
328
+ console.error('āŒ Error: A valid 40-hex Base contract address (0x...) is required.');
329
+ console.error('Usage: npx @m2msentinel/sdk audit 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\n');
330
+ return 1;
331
+ }
332
+ targetPath = `/v1/audit/${addr}`;
333
+ } else if (cmd === 'gas' || cmd === 'fees') {
334
+ targetPath = '/v1/gas/fees';
335
+ } else if (cmd === 'price') {
336
+ const symbol = (positional[1] || 'USDC').toUpperCase();
337
+ targetPath = `/v1/token/price/${encodeURIComponent(symbol)}`;
338
+ } else if (cmd === 'dex') {
339
+ targetPath = '/v1/dex/metrics';
340
+ } else if (cmd === 'whales') {
341
+ targetPath = '/v1/whales/signals';
342
+ } else if (cmd === 'status') {
343
+ targetPath = '/v1/status';
344
+ } else {
345
+ printHelp();
346
+ return 0;
347
+ }
348
+
349
+ try {
350
+ let res = await queryApi(targetPath, customApiKey, customBaseUrl);
351
+
352
+ // If unauthenticated / 401 / 402 on audit, try public demo endpoint
353
+ if (!res.ok && commandName === 'audit' && (res.status === 401 || res.status === 402)) {
354
+ const targetAddr = positional[1] || cmd;
355
+ const demoTarget = `/v1/demo/audit/${encodeURIComponent(targetAddr)}`;
356
+ const demoRes = await queryApi(demoTarget, null, customBaseUrl);
357
+ if (demoRes.ok) {
358
+ res = demoRes;
359
+ }
360
+ }
361
+
362
+ if (isJson) {
363
+ console.log(JSON.stringify(res.body, null, 2));
364
+ return res.ok ? 0 : 1;
365
+ }
366
+
367
+ if (!res.ok) {
368
+ console.error(`\nāŒ Request failed with HTTP ${res.status}`);
369
+ if (res.body && res.body.message) {
370
+ console.error(`Reason: ${res.body.message}`);
371
+ }
372
+ if (res.status === 402 || res.status === 401) {
373
+ console.error('šŸ’” This endpoint requires an API key or autonomous x402 payment.');
374
+ console.error('Pass a key via --api-key <key> or set M2M_SENTINEL_API_KEY.\n');
375
+ }
376
+ return 1;
377
+ }
378
+
379
+ const data = res.body;
380
+
381
+ if (commandName === 'audit') {
382
+ const auditObj = data.audit || data;
383
+ const targetAddress = auditObj.address || auditObj.contractAddress || data.contractAddress || positional[1] || cmd;
384
+ console.log('================================================================');
385
+ console.log('šŸ›”ļø M2M SENTINEL — BASE SMART CONTRACT AUDIT');
386
+ console.log(`Target Address: ${targetAddress}`);
387
+ console.log(`Network: Base Mainnet (8453)`);
388
+ console.log(`Latency: ${res.latencyMs}ms`);
389
+ console.log('================================================================');
390
+
391
+ const proxy = auditObj.proxyResolution || auditObj.proxy || {};
392
+ const isProxy = !!proxy.isProxy;
393
+ if (isProxy) {
394
+ const impl = proxy.targetAddress || proxy.implementation || (auditObj.reproducibility && auditObj.reproducibility.implementationAddress) || 'Uninstantiated (0x0)';
395
+ console.log(`• Proxy Standard: ${proxy.proxyType || (data.sample && data.sample.category) || 'EIP-1967 Proxy'}`);
396
+ console.log(`• Implementation: ${impl}`);
397
+ if (proxy.admin) console.log(`• Admin Address: ${proxy.admin}`);
398
+ } else {
399
+ console.log(`• Contract Architecture: Direct Implementation (Non-Proxy)`);
400
+ }
401
+
402
+ const caps = (auditObj.dissection && auditObj.dissection.capabilities) || auditObj.observedCapabilities || auditObj.capabilities || [];
403
+ if (Array.isArray(caps) && caps.length > 0) {
404
+ const capNames = caps.map(c => typeof c === 'string' ? c : (c.type || c.name || c.id || c.capability));
405
+ console.log(`• Capabilities: ${capNames.filter(Boolean).join(', ')}`);
406
+ }
407
+
408
+ const score = auditObj.capabilityScore !== undefined ? auditObj.capabilityScore : data.score;
409
+ if (score !== undefined) {
410
+ console.log(`• Capability Score: ${score}/100 (Coverage Index)`);
411
+ }
412
+
413
+ const trust = (auditObj.provenance && auditObj.provenance.trustLevel) || auditObj.trustLevel || 'HIGH';
414
+ console.log(`• Upstream Trust: ${trust}`);
415
+ console.log('----------------------------------------------------------------');
416
+ console.log('Notice: Factual static capability observation. Not a safety guarantee.');
417
+ console.log('šŸ’” Tip: Claim your free API key & track usage at https://m2msentinel.com');
418
+ console.log('================================================================\n');
419
+
420
+ } else if (commandName === 'gas' || commandName === 'fees') {
421
+ console.log('================================================================');
422
+ console.log('⛽ M2M SENTINEL — BASE GAS METRICS');
423
+ console.log(`Network: Base Mainnet (8453) | Sourced Latency: ${res.latencyMs}ms`);
424
+ console.log('================================================================');
425
+ console.log(`• Standard Gas Price: ${data.standard || data.gasPriceGwei || '0.005'} gwei`);
426
+ if (data.fast) console.log(`• Fast Gas Price: ${data.fast} gwei`);
427
+ if (data.instant) console.log(`• Instant Gas Price: ${data.instant} gwei`);
428
+ if (data.recommendation) console.log(`• Execution Advice: ${data.recommendation}`);
429
+ console.log('================================================================\n');
430
+
431
+ } else if (commandName === 'price') {
432
+ console.log('================================================================');
433
+ console.log(`šŸ“ˆ M2M SENTINEL — BASE DEX TOKEN PRICE`);
434
+ console.log(`Asset: ${data.symbol || positional[1]} | Latency: ${res.latencyMs}ms`);
435
+ console.log('================================================================');
436
+ console.log(`• Price (USD): $${data.priceUsd || data.price || 'N/A'}`);
437
+ if (data.tokenAddress) console.log(`• Contract Address: ${data.tokenAddress}`);
438
+ if (data.decimals) console.log(`• Decimals: ${data.decimals}`);
439
+ if (data.source) console.log(`• Sourced Pool: ${data.source}`);
440
+ console.log('================================================================\n');
441
+
442
+ } else if (commandName === 'status') {
443
+ console.log('================================================================');
444
+ console.log('🟢 M2M SENTINEL — OPERATIONAL STATUS');
445
+ console.log(`Status: ${data.status} | Network: Base Mainnet (8453)`);
446
+ console.log('================================================================');
447
+ if (data.components) {
448
+ for (const [comp, detail] of Object.entries(data.components)) {
449
+ console.log(`• ${comp.padEnd(20)}: ${detail.status} (${detail.mode || detail.activeProvider || 'OK'})`);
450
+ }
451
+ }
452
+ if (data.availability && data.availability.windows) {
453
+ console.log('----------------------------------------------------------------');
454
+ console.log('Availability Windows (Measured Uptime):');
455
+ for (const [w, win] of Object.entries(data.availability.windows)) {
456
+ console.log(` ${w.padEnd(6)}: ${win.availabilityPercent}% (${win.sampleCount} samples)`);
457
+ }
458
+ }
459
+ console.log('================================================================\n');
460
+
461
+ } else {
462
+ console.log(JSON.stringify(data, null, 2));
463
+ }
464
+
465
+ return 0;
466
+ } catch (err) {
467
+ console.error('āŒ Request error:', err.message);
468
+ return 1;
469
+ }
470
+ }
471
+
472
+ // ---------------------------------------------------------------------------
473
+ // Entrypoint: Dual Mode Selection
474
+ // ---------------------------------------------------------------------------
475
+
476
+ const cliArgs = process.argv.slice(2);
477
+
478
+ // If arguments are passed or run in an interactive terminal, invoke CLI
479
+ if (cliArgs.length > 0 || (process.stdin.isTTY && !process.env.M2M_MCP_FORCE)) {
480
+ if (cliArgs.length === 0) {
481
+ printHelp();
482
+ } else {
483
+ runCli(cliArgs).then((code) => {
484
+ if (code !== 0 && typeof code === 'number') process.exit(code);
485
+ });
486
+ }
487
+ } else {
488
+ // Piped execution without args -> start MCP stdio server
489
+ startMcpServer();
490
+ }
491
+
492
+ module.exports = { TOOLS, pathForTool, parseX402Header, runCli, queryApi };