@proofofprotocol/inscribe-mcp 0.1.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/LICENSE +21 -0
- package/README.md +255 -0
- package/package.json +58 -0
- package/src/cli/commands/balance.js +151 -0
- package/src/cli/commands/config.js +87 -0
- package/src/cli/commands/init.js +186 -0
- package/src/cli/commands/log.js +193 -0
- package/src/cli/commands/show.js +249 -0
- package/src/cli/index.js +44 -0
- package/src/cli/lib/config.js +126 -0
- package/src/cli/lib/exit-codes.js +19 -0
- package/src/lib/did.js +64 -0
- package/src/lib/hedera.js +221 -0
- package/src/lib/logger.js +200 -0
- package/src/server-sse.js +239 -0
- package/src/server.js +102 -0
- package/src/test.js +107 -0
- package/src/tools/layer1/history.js +174 -0
- package/src/tools/layer1/identity.js +120 -0
- package/src/tools/layer1/inscribe.js +132 -0
- package/src/tools/layer1/inscribe_url.js +193 -0
- package/src/tools/layer1/verify.js +177 -0
- package/src/tools/layer2/account.js +155 -0
- package/src/tools/layer2/hcs.js +163 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Management
|
|
3
|
+
*
|
|
4
|
+
* Read/write ~/.inscribe-mcp/config.json
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.inscribe-mcp');
|
|
12
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get config directory path
|
|
16
|
+
*/
|
|
17
|
+
export function getConfigDir() {
|
|
18
|
+
return CONFIG_DIR;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get config file path
|
|
23
|
+
*/
|
|
24
|
+
export function getConfigPath() {
|
|
25
|
+
return CONFIG_FILE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if config exists
|
|
30
|
+
*/
|
|
31
|
+
export function configExists() {
|
|
32
|
+
return existsSync(CONFIG_FILE);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read config
|
|
37
|
+
* @returns {Object|null} Config object or null if not found
|
|
38
|
+
*/
|
|
39
|
+
export function readConfig() {
|
|
40
|
+
if (!configExists()) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
46
|
+
return JSON.parse(content);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write config
|
|
54
|
+
* @param {Object} config - Config object to write
|
|
55
|
+
*/
|
|
56
|
+
export function writeConfig(config) {
|
|
57
|
+
// Ensure directory exists
|
|
58
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
59
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate config structure
|
|
67
|
+
* @param {Object} config - Config to validate
|
|
68
|
+
* @returns {Object} { valid: boolean, errors: string[] }
|
|
69
|
+
*/
|
|
70
|
+
export function validateConfig(config) {
|
|
71
|
+
const errors = [];
|
|
72
|
+
|
|
73
|
+
if (!config) {
|
|
74
|
+
errors.push('config is null or undefined');
|
|
75
|
+
return { valid: false, errors };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!config.network) {
|
|
79
|
+
errors.push('network is required');
|
|
80
|
+
} else if (!['testnet', 'mainnet'].includes(config.network)) {
|
|
81
|
+
errors.push('network must be "testnet" or "mainnet"');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!config.operatorAccountId) {
|
|
85
|
+
errors.push('operatorAccountId is required');
|
|
86
|
+
} else if (!/^0\.0\.\d+$/.test(config.operatorAccountId)) {
|
|
87
|
+
errors.push('operatorAccountId must be in format 0.0.XXXXX');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!config.operatorPrivateKey) {
|
|
91
|
+
errors.push('operatorPrivateKey is required');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// defaultTopicId is optional (will be auto-created)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
valid: errors.length === 0,
|
|
98
|
+
errors
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Mask private key for display
|
|
104
|
+
* @param {string} key - Private key
|
|
105
|
+
* @returns {string} Masked key
|
|
106
|
+
*/
|
|
107
|
+
export function maskPrivateKey(key) {
|
|
108
|
+
if (!key || key.length < 16) {
|
|
109
|
+
return '********';
|
|
110
|
+
}
|
|
111
|
+
return key.substring(0, 8) + '...' + key.substring(key.length - 8);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get config for display (with masked private key)
|
|
116
|
+
* @returns {Object|null} Config with masked private key
|
|
117
|
+
*/
|
|
118
|
+
export function getDisplayConfig() {
|
|
119
|
+
const config = readConfig();
|
|
120
|
+
if (!config) return null;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...config,
|
|
124
|
+
operatorPrivateKey: maskPrivateKey(config.operatorPrivateKey)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Exit Codes
|
|
3
|
+
*
|
|
4
|
+
* Standard exit codes for inscribe-mcp CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const EXIT_CODES = {
|
|
8
|
+
SUCCESS: 0, // 正常終了
|
|
9
|
+
CONFIG_ERROR: 1, // 設定不備(config.json 未作成など)
|
|
10
|
+
LOG_NOT_FOUND: 2, // ログ未検出
|
|
11
|
+
NETWORK_ERROR: 3 // ネットワーク/API エラー
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const EXIT_MESSAGES = {
|
|
15
|
+
[EXIT_CODES.SUCCESS]: '正常終了',
|
|
16
|
+
[EXIT_CODES.CONFIG_ERROR]: '設定エラー: config.json が見つからないか不正です',
|
|
17
|
+
[EXIT_CODES.LOG_NOT_FOUND]: 'ログが見つかりません',
|
|
18
|
+
[EXIT_CODES.NETWORK_ERROR]: 'ネットワークエラー: API への接続に失敗しました'
|
|
19
|
+
};
|
package/src/lib/did.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DID Utilities
|
|
3
|
+
*
|
|
4
|
+
* Normalize and validate Hedera DIDs.
|
|
5
|
+
* Standard format: did:hedera:{network}:{accountId}
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getMirrorNodeUrl } from './hedera.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize author to standard DID format
|
|
12
|
+
* Accepts: Account ID, EVM address, or full DID
|
|
13
|
+
* Returns: did:hedera:{network}:{accountId} or null
|
|
14
|
+
*/
|
|
15
|
+
export async function normalizeDid(author) {
|
|
16
|
+
if (!author) return null;
|
|
17
|
+
|
|
18
|
+
const network = process.env.HEDERA_NETWORK || 'testnet';
|
|
19
|
+
|
|
20
|
+
// Already standard DID format with Account ID
|
|
21
|
+
const didMatch = author.match(/^did:hedera:\w+:(0\.0\.\d+)$/);
|
|
22
|
+
if (didMatch) {
|
|
23
|
+
return `did:hedera:${network}:${didMatch[1]}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Account ID only (0.0.xxxxx)
|
|
27
|
+
const accountMatch = author.match(/^(0\.0\.\d+)$/);
|
|
28
|
+
if (accountMatch) {
|
|
29
|
+
return `did:hedera:${network}:${accountMatch[1]}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// EVM address (with or without 0x, or in DID format)
|
|
33
|
+
const evmMatch = author.match(/(?:did:hedera:\w+:)?(?:0x)?([a-fA-F0-9]{40})$/);
|
|
34
|
+
if (evmMatch) {
|
|
35
|
+
const evmAddress = evmMatch[1].toLowerCase();
|
|
36
|
+
// Try to resolve to Account ID
|
|
37
|
+
const accountId = await evmToAccountId(evmAddress);
|
|
38
|
+
if (accountId) {
|
|
39
|
+
return `did:hedera:${network}:${accountId}`;
|
|
40
|
+
}
|
|
41
|
+
// Fallback: return EVM-based DID (not ideal but valid)
|
|
42
|
+
return `did:hedera:${network}:0x${evmAddress}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Unknown format - return as-is wrapped in DID
|
|
46
|
+
return `did:hedera:${network}:${author}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve EVM address to Account ID via Mirror Node
|
|
51
|
+
*/
|
|
52
|
+
async function evmToAccountId(evmAddress) {
|
|
53
|
+
try {
|
|
54
|
+
const mirrorUrl = getMirrorNodeUrl();
|
|
55
|
+
const response = await fetch(`${mirrorUrl}/api/v1/accounts/0x${evmAddress}`);
|
|
56
|
+
if (response.ok) {
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
return data.account || null;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore errors
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hedera Client Library
|
|
3
|
+
* Shared utilities for Hedera network interactions
|
|
4
|
+
*
|
|
5
|
+
* Configuration priority:
|
|
6
|
+
* 1. ~/.inscribe-mcp/config.json (if exists)
|
|
7
|
+
* 2. Environment variables / .env (fallback)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Client, AccountId, PrivateKey, TopicId, TopicCreateTransaction } from '@hashgraph/sdk';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import dotenv from 'dotenv';
|
|
15
|
+
|
|
16
|
+
dotenv.config();
|
|
17
|
+
|
|
18
|
+
let client = null;
|
|
19
|
+
let cachedConfig = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load configuration from config.json or environment
|
|
23
|
+
*/
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
if (cachedConfig) return cachedConfig;
|
|
26
|
+
|
|
27
|
+
const configPath = join(homedir(), '.inscribe-mcp', 'config.json');
|
|
28
|
+
|
|
29
|
+
// Try config.json first
|
|
30
|
+
if (existsSync(configPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
33
|
+
cachedConfig = JSON.parse(content);
|
|
34
|
+
return cachedConfig;
|
|
35
|
+
} catch {
|
|
36
|
+
// Fall through to env
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback to environment variables
|
|
41
|
+
cachedConfig = {
|
|
42
|
+
network: process.env.HEDERA_NETWORK || 'testnet',
|
|
43
|
+
operatorAccountId: process.env.HEDERA_OPERATOR_ID || process.env.HEDERA_ACCOUNT_ID,
|
|
44
|
+
operatorPrivateKey: process.env.HEDERA_OPERATOR_KEY || process.env.HEDERA_PRIVATE_KEY,
|
|
45
|
+
defaultTopicId: process.env.DEFAULT_TOPIC_ID
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return cachedConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get or create Hedera client
|
|
53
|
+
*/
|
|
54
|
+
export function getHederaClient() {
|
|
55
|
+
if (client) return client;
|
|
56
|
+
|
|
57
|
+
const config = loadConfig();
|
|
58
|
+
const { network, operatorAccountId, operatorPrivateKey } = config;
|
|
59
|
+
|
|
60
|
+
if (!operatorAccountId || !operatorPrivateKey) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'Hedera credentials not found.\n' +
|
|
63
|
+
'Run "npx inscribe-mcp init" to configure, or set environment variables.'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Parse private key (supports DER and raw formats)
|
|
68
|
+
let privateKey;
|
|
69
|
+
if (operatorPrivateKey.startsWith('302')) {
|
|
70
|
+
privateKey = PrivateKey.fromStringDer(operatorPrivateKey);
|
|
71
|
+
} else if (operatorPrivateKey.startsWith('0x')) {
|
|
72
|
+
privateKey = PrivateKey.fromStringECDSA(operatorPrivateKey);
|
|
73
|
+
} else {
|
|
74
|
+
privateKey = PrivateKey.fromString(operatorPrivateKey);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
client = network === 'mainnet'
|
|
78
|
+
? Client.forMainnet()
|
|
79
|
+
: Client.forTestnet();
|
|
80
|
+
|
|
81
|
+
client.setOperator(AccountId.fromString(operatorAccountId), privateKey);
|
|
82
|
+
|
|
83
|
+
return client;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get operator account ID
|
|
88
|
+
*/
|
|
89
|
+
export function getOperatorId() {
|
|
90
|
+
const config = loadConfig();
|
|
91
|
+
return AccountId.fromString(config.operatorAccountId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get default topic ID for inscriptions
|
|
96
|
+
* Returns null if not configured (caller should use getOrCreateTopicId instead)
|
|
97
|
+
*/
|
|
98
|
+
export function getTopicId() {
|
|
99
|
+
const config = loadConfig();
|
|
100
|
+
const topicId = config.defaultTopicId;
|
|
101
|
+
if (!topicId) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return TopicId.fromString(topicId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if topic ID is configured
|
|
109
|
+
*/
|
|
110
|
+
export function hasTopicId() {
|
|
111
|
+
const config = loadConfig();
|
|
112
|
+
return !!config.defaultTopicId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get config file path
|
|
117
|
+
*/
|
|
118
|
+
function getConfigPath() {
|
|
119
|
+
return join(homedir(), '.inscribe-mcp', 'config.json');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Save topic ID to config.json
|
|
124
|
+
*/
|
|
125
|
+
export function saveTopicId(topicId) {
|
|
126
|
+
const configPath = getConfigPath();
|
|
127
|
+
const configDir = join(homedir(), '.inscribe-mcp');
|
|
128
|
+
|
|
129
|
+
// Ensure directory exists
|
|
130
|
+
if (!existsSync(configDir)) {
|
|
131
|
+
mkdirSync(configDir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Read existing config or create new
|
|
135
|
+
let config = {};
|
|
136
|
+
if (existsSync(configPath)) {
|
|
137
|
+
try {
|
|
138
|
+
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
139
|
+
} catch {
|
|
140
|
+
// Start fresh
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Update topic ID
|
|
145
|
+
config.defaultTopicId = topicId.toString();
|
|
146
|
+
|
|
147
|
+
// Write back
|
|
148
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
149
|
+
|
|
150
|
+
// Clear cache so next read picks up new value
|
|
151
|
+
clearConfigCache();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a new topic and save to config
|
|
156
|
+
* @returns {Promise<TopicId>}
|
|
157
|
+
*/
|
|
158
|
+
export async function createAndSaveTopic() {
|
|
159
|
+
const client = getHederaClient();
|
|
160
|
+
|
|
161
|
+
const transaction = new TopicCreateTransaction()
|
|
162
|
+
.setTopicMemo('inscribe-mcp topic');
|
|
163
|
+
|
|
164
|
+
const response = await transaction.execute(client);
|
|
165
|
+
const receipt = await response.getReceipt(client);
|
|
166
|
+
const topicId = receipt.topicId;
|
|
167
|
+
|
|
168
|
+
// Save to config
|
|
169
|
+
saveTopicId(topicId);
|
|
170
|
+
|
|
171
|
+
return topicId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get topic ID, creating one if not configured
|
|
176
|
+
* @returns {Promise<TopicId>}
|
|
177
|
+
*/
|
|
178
|
+
export async function getOrCreateTopicId() {
|
|
179
|
+
const existing = getTopicId();
|
|
180
|
+
if (existing) {
|
|
181
|
+
return existing;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create new topic
|
|
185
|
+
return await createAndSaveTopic();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get network name
|
|
190
|
+
*/
|
|
191
|
+
export function getNetwork() {
|
|
192
|
+
const config = loadConfig();
|
|
193
|
+
return config.network || 'testnet';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get Mirror Node URL based on network
|
|
198
|
+
*/
|
|
199
|
+
export function getMirrorNodeUrl() {
|
|
200
|
+
const network = getNetwork();
|
|
201
|
+
return network === 'mainnet'
|
|
202
|
+
? 'https://mainnet.mirrornode.hedera.com'
|
|
203
|
+
: 'https://testnet.mirrornode.hedera.com';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clear cached config (for testing or reloading)
|
|
208
|
+
*/
|
|
209
|
+
export function clearConfigCache() {
|
|
210
|
+
cachedConfig = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Close the client connection
|
|
215
|
+
*/
|
|
216
|
+
export function closeClient() {
|
|
217
|
+
if (client) {
|
|
218
|
+
client.close();
|
|
219
|
+
client = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger - JSONL structured logging
|
|
3
|
+
*
|
|
4
|
+
* Append-only logging to ~/.inscribe-mcp/logs/YYYY-MM-DD.jsonl
|
|
5
|
+
* Used by MCP Server to record all tool executions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, appendFileSync } from 'fs';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
const LOGS_DIR = join(homedir(), '.inscribe-mcp', 'logs');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensure logs directory exists
|
|
16
|
+
*/
|
|
17
|
+
function ensureLogsDir() {
|
|
18
|
+
if (!existsSync(LOGS_DIR)) {
|
|
19
|
+
mkdirSync(LOGS_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get current log file path (YYYY-MM-DD.jsonl)
|
|
25
|
+
*/
|
|
26
|
+
function getLogFilePath() {
|
|
27
|
+
const date = new Date().toISOString().split('T')[0];
|
|
28
|
+
return join(LOGS_DIR, `${date}.jsonl`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get logs directory path
|
|
33
|
+
*/
|
|
34
|
+
export function getLogsDir() {
|
|
35
|
+
return LOGS_DIR;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write a log entry
|
|
40
|
+
* @param {Object} entry - Log entry object
|
|
41
|
+
*/
|
|
42
|
+
export function writeLog(entry) {
|
|
43
|
+
ensureLogsDir();
|
|
44
|
+
|
|
45
|
+
const logEntry = {
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
...entry
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const line = JSON.stringify(logEntry) + '\n';
|
|
51
|
+
appendFileSync(getLogFilePath(), line, 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Log a tool execution (standard format)
|
|
56
|
+
* @param {Object} options - Log options
|
|
57
|
+
*/
|
|
58
|
+
export function logToolExecution({
|
|
59
|
+
command,
|
|
60
|
+
network,
|
|
61
|
+
topicId = null,
|
|
62
|
+
operator,
|
|
63
|
+
status,
|
|
64
|
+
latency,
|
|
65
|
+
txId = null,
|
|
66
|
+
error = null,
|
|
67
|
+
metadata = {}
|
|
68
|
+
}) {
|
|
69
|
+
writeLog({
|
|
70
|
+
level: status === 'success' ? 'info' : 'error',
|
|
71
|
+
command,
|
|
72
|
+
network,
|
|
73
|
+
topicId,
|
|
74
|
+
operator,
|
|
75
|
+
status,
|
|
76
|
+
latency,
|
|
77
|
+
txId,
|
|
78
|
+
error,
|
|
79
|
+
...metadata
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Log a debug sequence trace
|
|
85
|
+
* @param {Object} options - Debug trace options
|
|
86
|
+
*/
|
|
87
|
+
export function logDebugTrace({
|
|
88
|
+
phase,
|
|
89
|
+
tool = null,
|
|
90
|
+
payload = null,
|
|
91
|
+
status = null,
|
|
92
|
+
txId = null,
|
|
93
|
+
result = null
|
|
94
|
+
}) {
|
|
95
|
+
// Only log if debug mode is enabled
|
|
96
|
+
if (!process.env.INSCRIBE_MCP_DEBUG) return;
|
|
97
|
+
|
|
98
|
+
writeLog({
|
|
99
|
+
level: 'debug',
|
|
100
|
+
phase,
|
|
101
|
+
tool,
|
|
102
|
+
payload,
|
|
103
|
+
status,
|
|
104
|
+
txId,
|
|
105
|
+
result
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create a timer for measuring latency
|
|
111
|
+
* @returns {Function} Function that returns elapsed ms when called
|
|
112
|
+
*/
|
|
113
|
+
export function createTimer() {
|
|
114
|
+
const start = Date.now();
|
|
115
|
+
return () => Date.now() - start;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Helper to wrap tool execution with logging
|
|
120
|
+
* @param {string} command - Tool name
|
|
121
|
+
* @param {Function} fn - Async function to execute
|
|
122
|
+
* @param {Object} options - Additional log options
|
|
123
|
+
*/
|
|
124
|
+
export async function withLogging(command, fn, options = {}) {
|
|
125
|
+
const timer = createTimer();
|
|
126
|
+
const network = options.network || 'unknown';
|
|
127
|
+
const operator = options.operator || 'unknown';
|
|
128
|
+
const topicId = options.topicId || null;
|
|
129
|
+
|
|
130
|
+
// Debug: agent_to_mcp
|
|
131
|
+
logDebugTrace({
|
|
132
|
+
phase: 'agent_to_mcp',
|
|
133
|
+
tool: command,
|
|
134
|
+
payload: options.payload
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Debug: mcp_to_chain
|
|
139
|
+
logDebugTrace({
|
|
140
|
+
phase: 'mcp_to_chain',
|
|
141
|
+
tool: command
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = await fn();
|
|
145
|
+
|
|
146
|
+
// Debug: chain_to_mcp
|
|
147
|
+
logDebugTrace({
|
|
148
|
+
phase: 'chain_to_mcp',
|
|
149
|
+
status: 'SUCCESS',
|
|
150
|
+
txId: result?.txId || result?.inscription_id
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Debug: mcp_to_agent
|
|
154
|
+
logDebugTrace({
|
|
155
|
+
phase: 'mcp_to_agent',
|
|
156
|
+
result: result?.success ? 'success' : 'partial'
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Standard log
|
|
160
|
+
logToolExecution({
|
|
161
|
+
command,
|
|
162
|
+
network,
|
|
163
|
+
topicId,
|
|
164
|
+
operator,
|
|
165
|
+
status: 'success',
|
|
166
|
+
latency: timer(),
|
|
167
|
+
txId: result?.txId || result?.inscription_id,
|
|
168
|
+
metadata: options.metadata
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
|
|
173
|
+
} catch (error) {
|
|
174
|
+
// Debug: chain_to_mcp (error)
|
|
175
|
+
logDebugTrace({
|
|
176
|
+
phase: 'chain_to_mcp',
|
|
177
|
+
status: 'FAILED'
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Debug: mcp_to_agent (error)
|
|
181
|
+
logDebugTrace({
|
|
182
|
+
phase: 'mcp_to_agent',
|
|
183
|
+
result: 'error'
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Standard error log
|
|
187
|
+
logToolExecution({
|
|
188
|
+
command,
|
|
189
|
+
network,
|
|
190
|
+
topicId,
|
|
191
|
+
operator,
|
|
192
|
+
status: 'error',
|
|
193
|
+
latency: timer(),
|
|
194
|
+
error: error.message,
|
|
195
|
+
metadata: options.metadata
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|