@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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * identity - Trust Identity Creation Tool
3
+ *
4
+ * Create a Hedera-based identity (DID) for inscriptions.
5
+ */
6
+
7
+ import { PrivateKey, Hbar, TransferTransaction } from '@hashgraph/sdk';
8
+ import { getHederaClient, getOperatorId, getMirrorNodeUrl } from '../../lib/hedera.js';
9
+
10
+ export const identityTool = {
11
+ name: 'identity',
12
+ description: '信頼IDを作成する(Hederaアカウント + DID)。inscribe 時の author として使用可能。',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ name: {
17
+ type: 'string',
18
+ description: 'IDの名前(オプション、識別用)'
19
+ },
20
+ initial_balance: {
21
+ type: 'number',
22
+ description: '初期残高(HBAR)。デフォルト: 1 HBAR',
23
+ default: 1
24
+ }
25
+ }
26
+ },
27
+
28
+ handler: async ({ name, initial_balance = 1 }) => {
29
+ const client = getHederaClient();
30
+ const operatorId = getOperatorId();
31
+
32
+ // Generate new ECDSA key pair
33
+ const privateKey = PrivateKey.generateECDSA();
34
+ const publicKey = privateKey.publicKey;
35
+
36
+ // EVM Address (alias)
37
+ const evmAddress = publicKey.toEvmAddress();
38
+
39
+ try {
40
+ // Auto-create account by transferring HBAR
41
+ const transferTx = new TransferTransaction()
42
+ .addHbarTransfer(operatorId, new Hbar(-initial_balance))
43
+ .addHbarTransfer(evmAddress, new Hbar(initial_balance));
44
+
45
+ const response = await transferTx.execute(client);
46
+ await response.getReceipt(client);
47
+
48
+ // Retry logic for Mirror Node indexing delay
49
+ const mirrorUrl = getMirrorNodeUrl();
50
+ let newAccountId = null;
51
+
52
+ for (let attempt = 1; attempt <= 5; attempt++) {
53
+ // Wait before checking (increasing delay)
54
+ await new Promise(resolve => setTimeout(resolve, attempt * 2000));
55
+
56
+ // Use EVM address for lookup (more reliable than public key)
57
+ const mirrorResponse = await fetch(
58
+ `${mirrorUrl}/api/v1/accounts/0x${evmAddress}`
59
+ );
60
+
61
+ if (mirrorResponse.ok) {
62
+ const mirrorData = await mirrorResponse.json();
63
+ newAccountId = mirrorData.account;
64
+ if (newAccountId) break;
65
+ }
66
+ }
67
+
68
+ if (!newAccountId) {
69
+ // Account created but ID not yet indexed - still return useful info
70
+ const network = process.env.HEDERA_NETWORK || 'testnet';
71
+ return {
72
+ success: true,
73
+ partial: true,
74
+ identity: {
75
+ did: `did:hedera:${network}:${evmAddress}`,
76
+ account_id: null,
77
+ evm_address: evmAddress,
78
+ public_key: publicKey.toStringRaw(),
79
+ name: name || null
80
+ },
81
+ credentials: {
82
+ private_key: privateKey.toStringRaw(),
83
+ private_key_der: privateKey.toStringDer(),
84
+ warning: 'この秘密鍵は安全に保管してください。再発行できません。'
85
+ },
86
+ balance: `${initial_balance} HBAR`,
87
+ message: 'アカウント作成成功。Account IDは数分後にHashScanで確認できます。',
88
+ hashscan_url: `https://hashscan.io/${network}/account/${evmAddress}`
89
+ };
90
+ }
91
+
92
+ // DID format
93
+ const network = process.env.HEDERA_NETWORK || 'testnet';
94
+ const did = `did:hedera:${network}:${newAccountId}`;
95
+
96
+ return {
97
+ success: true,
98
+ identity: {
99
+ did,
100
+ account_id: newAccountId,
101
+ evm_address: evmAddress,
102
+ public_key: publicKey.toStringRaw(),
103
+ name: name || null
104
+ },
105
+ credentials: {
106
+ private_key: privateKey.toStringRaw(),
107
+ private_key_der: privateKey.toStringDer(),
108
+ warning: 'この秘密鍵は安全に保管してください。再発行できません。'
109
+ },
110
+ balance: `${initial_balance} HBAR`,
111
+ message: '信頼IDが作成されました。inscribe 時に author として使用できます。'
112
+ };
113
+ } catch (error) {
114
+ return {
115
+ success: false,
116
+ error: `ID作成エラー: ${error.message}`
117
+ };
118
+ }
119
+ }
120
+ };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * inscribe - Text Inscription Tool
3
+ *
4
+ * Inscribe any text content to blockchain with timestamp proof.
5
+ */
6
+
7
+ import { createHash } from 'crypto';
8
+ import { TopicMessageSubmitTransaction } from '@hashgraph/sdk';
9
+ import { getHederaClient, getOrCreateTopicId, getNetwork, getOperatorId, hasTopicId } from '../../lib/hedera.js';
10
+ import { normalizeDid } from '../../lib/did.js';
11
+ import { logToolExecution, createTimer } from '../../lib/logger.js';
12
+
13
+ export const inscribeTool = {
14
+ name: 'inscribe',
15
+ description: '内容を刻む(存在証明・タイムスタンプ)。ブロックチェーンに永続的に記録される。',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ content: {
20
+ type: 'string',
21
+ description: '刻む内容(テキスト、ハッシュ、JSON など)'
22
+ },
23
+ type: {
24
+ type: 'string',
25
+ description: '種類(article, log, decision, contract, prediction など)',
26
+ default: 'general'
27
+ },
28
+ author: {
29
+ type: 'string',
30
+ description: '著者ID(DID形式、省略可)'
31
+ },
32
+ metadata: {
33
+ type: 'object',
34
+ description: '追加メタデータ(省略可)'
35
+ }
36
+ },
37
+ required: ['content']
38
+ },
39
+
40
+ handler: async ({ content, type = 'general', author, metadata }) => {
41
+ const timer = createTimer();
42
+ const client = getHederaClient();
43
+ const network = getNetwork();
44
+ const operator = getOperatorId().toString();
45
+
46
+ // Check if topic needs to be created
47
+ const topicWasCreated = !hasTopicId();
48
+
49
+ try {
50
+ // Get or create topic
51
+ const topicId = await getOrCreateTopicId();
52
+
53
+ // Log topic creation if it was just created
54
+ if (topicWasCreated) {
55
+ logToolExecution({
56
+ command: 'topic_create',
57
+ network,
58
+ topicId: topicId.toString(),
59
+ operator,
60
+ status: 'success',
61
+ latency: 0,
62
+ metadata: { autoCreated: true }
63
+ });
64
+ }
65
+ // Generate content hash
66
+ const contentHash = createHash('sha256').update(content).digest('hex');
67
+
68
+ // Normalize author DID to standard format
69
+ const normalizedAuthor = await normalizeDid(author);
70
+
71
+ // Build inscription data
72
+ const inscription = {
73
+ version: '1.0',
74
+ type,
75
+ content_hash: contentHash,
76
+ content_preview: content.substring(0, 100) + (content.length > 100 ? '...' : ''),
77
+ content_full: content,
78
+ author: normalizedAuthor,
79
+ metadata: metadata || {},
80
+ inscribed_at: new Date().toISOString()
81
+ };
82
+
83
+ // Submit to HCS
84
+ const transaction = new TopicMessageSubmitTransaction()
85
+ .setTopicId(topicId)
86
+ .setMessage(JSON.stringify(inscription));
87
+
88
+ const response = await transaction.execute(client);
89
+ const record = await response.getRecord(client);
90
+ const timestamp = record.consensusTimestamp.toString();
91
+ const inscriptionId = `${topicId}-${timestamp}`;
92
+
93
+ // Log success
94
+ logToolExecution({
95
+ command: 'inscribe',
96
+ network,
97
+ topicId: topicId.toString(),
98
+ operator,
99
+ status: 'success',
100
+ latency: timer(),
101
+ txId: inscriptionId,
102
+ metadata: { type, contentLength: content.length }
103
+ });
104
+
105
+ return {
106
+ success: true,
107
+ inscription_id: inscriptionId,
108
+ content_hash: contentHash,
109
+ timestamp,
110
+ type,
111
+ verify_url: `https://hashscan.io/${network}/topic/${topicId}`,
112
+ message: '正常に刻まれました。この記録は改ざん不可能です。'
113
+ };
114
+
115
+ } catch (error) {
116
+ // Log error
117
+ logToolExecution({
118
+ command: 'inscribe',
119
+ network,
120
+ operator,
121
+ status: 'error',
122
+ latency: timer(),
123
+ error: error.message
124
+ });
125
+
126
+ return {
127
+ success: false,
128
+ error: error.message
129
+ };
130
+ }
131
+ }
132
+ };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * inscribe_url - URL Inscription Tool (Killer Feature)
3
+ *
4
+ * Inscribe any URL content to blockchain.
5
+ * Supports automatic content fetching from X/Twitter, YouTube, etc.
6
+ */
7
+
8
+ import { createHash } from 'crypto';
9
+ import { TopicMessageSubmitTransaction } from '@hashgraph/sdk';
10
+ import { getHederaClient, getTopicId, getMirrorNodeUrl } from '../../lib/hedera.js';
11
+
12
+ export const inscribeUrlTool = {
13
+ name: 'inscribe_url',
14
+ description: 'URLの内容を刻む。SNS投稿、ブログ記事、GitHub Issue など。自動取得できない場合は content で手動指定(目撃証言として記録)。',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ url: {
19
+ type: 'string',
20
+ description: '刻みたいURL(X/Twitter, YouTube, ブログなど)'
21
+ },
22
+ content: {
23
+ type: 'string',
24
+ description: '内容(自動取得できない場合、または目撃証言として手動指定)'
25
+ },
26
+ note: {
27
+ type: 'string',
28
+ description: '追加メモ(なぜ刻むのか、文脈など)'
29
+ }
30
+ },
31
+ required: ['url']
32
+ },
33
+
34
+ handler: async ({ url, content, note }) => {
35
+ const client = getHederaClient();
36
+ const topicId = getTopicId();
37
+
38
+ let fetchedContent = null;
39
+ let authorName = null;
40
+ const platform = detectPlatform(url);
41
+ let source = 'manual';
42
+
43
+ // Platform-specific content fetching
44
+ if (!content) {
45
+ if (platform === 'x') {
46
+ const result = await fetchFromX(url);
47
+ if (result) {
48
+ fetchedContent = result.content;
49
+ authorName = result.author;
50
+ source = 'oembed';
51
+ }
52
+ } else if (platform === 'youtube') {
53
+ const result = await fetchFromYouTube(url);
54
+ if (result) {
55
+ fetchedContent = result.content;
56
+ authorName = result.author;
57
+ source = 'oembed';
58
+ }
59
+ }
60
+ }
61
+
62
+ // Determine final content
63
+ const finalContent = content || fetchedContent;
64
+
65
+ if (!finalContent) {
66
+ const platformHints = {
67
+ x: 'ツイートの本文をコピーして content に渡してください',
68
+ youtube: '動画タイトルをコピーして content に渡してください',
69
+ github: 'Issue/PRのタイトルや内容をコピーして content に渡してください',
70
+ web: 'ページの重要な部分をコピーして content に渡してください'
71
+ };
72
+
73
+ return {
74
+ success: false,
75
+ error: '内容を自動取得できませんでした',
76
+ url,
77
+ platform,
78
+ hint: platformHints[platform] || platformHints.web,
79
+ example: `inscribe_url({ url: "${url}", content: "ここに内容を貼り付け" })`,
80
+ note: '手動で指定した内容は「目撃証言」として記録されます。自動取得と同じく改ざん不可能な証拠になります。'
81
+ };
82
+ }
83
+
84
+ // Generate hashes
85
+ const contentHash = createHash('sha256').update(finalContent).digest('hex');
86
+ const urlHash = createHash('sha256').update(url).digest('hex');
87
+
88
+ // Build inscription data
89
+ const inscription = {
90
+ version: '1.0',
91
+ type: 'url_capture',
92
+ platform,
93
+ source,
94
+ url,
95
+ url_hash: urlHash,
96
+ author: authorName,
97
+ content: finalContent,
98
+ content_hash: contentHash,
99
+ note: note || null,
100
+ captured_at: new Date().toISOString()
101
+ };
102
+
103
+ // Submit to HCS
104
+ const transaction = new TopicMessageSubmitTransaction()
105
+ .setTopicId(topicId)
106
+ .setMessage(JSON.stringify(inscription));
107
+
108
+ const response = await transaction.execute(client);
109
+ const record = await response.getRecord(client);
110
+ const timestamp = record.consensusTimestamp.toString();
111
+ const inscriptionId = `${topicId}-${timestamp}`;
112
+
113
+ const network = process.env.HEDERA_NETWORK || 'testnet';
114
+
115
+ return {
116
+ success: true,
117
+ inscription_id: inscriptionId,
118
+ capture: {
119
+ url,
120
+ platform,
121
+ source,
122
+ author: authorName,
123
+ content_preview: finalContent.substring(0, 200) + (finalContent.length > 200 ? '...' : ''),
124
+ content_hash: contentHash
125
+ },
126
+ timestamp,
127
+ verify_url: `https://hashscan.io/${network}/topic/${topicId}`,
128
+ message: source === 'oembed'
129
+ ? 'この投稿が存在した証拠を自動取得して刻みました'
130
+ : 'あなたの目撃証言として刻みました'
131
+ };
132
+ }
133
+ };
134
+
135
+ /**
136
+ * Detect platform from URL
137
+ */
138
+ function detectPlatform(url) {
139
+ if (url.includes('x.com') || url.includes('twitter.com')) return 'x';
140
+ if (url.includes('youtube.com') || url.includes('youtu.be')) return 'youtube';
141
+ if (url.includes('github.com')) return 'github';
142
+ if (url.includes('note.com')) return 'note';
143
+ if (url.includes('zenn.dev')) return 'zenn';
144
+ if (url.includes('qiita.com')) return 'qiita';
145
+ return 'web';
146
+ }
147
+
148
+ /**
149
+ * Fetch content from X/Twitter using oEmbed
150
+ */
151
+ async function fetchFromX(url) {
152
+ try {
153
+ const oembedUrl = `https://publish.twitter.com/oembed?url=${encodeURIComponent(url)}`;
154
+ const response = await fetch(oembedUrl);
155
+ if (!response.ok) return null;
156
+
157
+ const data = await response.json();
158
+
159
+ // Extract text from HTML
160
+ const htmlContent = data.html || '';
161
+ const textMatch = htmlContent.match(/<p[^>]*>([\s\S]*?)<\/p>/);
162
+ const content = textMatch
163
+ ? textMatch[1].replace(/<[^>]*>/g, '').replace(/&mdash;/g, '—').replace(/&amp;/g, '&').trim()
164
+ : null;
165
+
166
+ return content ? {
167
+ content,
168
+ author: data.author_name
169
+ } : null;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Fetch content from YouTube using oEmbed
177
+ */
178
+ async function fetchFromYouTube(url) {
179
+ try {
180
+ const oembedUrl = `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`;
181
+ const response = await fetch(oembedUrl);
182
+ if (!response.ok) return null;
183
+
184
+ const data = await response.json();
185
+
186
+ return {
187
+ content: data.title,
188
+ author: data.author_name
189
+ };
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * verify - Inscription Verification Tool
3
+ *
4
+ * Verify that inscribed content has not been tampered with.
5
+ */
6
+
7
+ import { createHash } from 'crypto';
8
+ import { getMirrorNodeUrl, getNetwork, getOperatorId } from '../../lib/hedera.js';
9
+ import { logToolExecution, createTimer } from '../../lib/logger.js';
10
+
11
+ export const verifyTool = {
12
+ name: 'verify',
13
+ description: '刻まれた内容を検証する。内容が改ざんされていないか確認。',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ inscription_id: {
18
+ type: 'string',
19
+ description: '検証する inscription ID (例: 0.0.7503789-1703412000.123456789)'
20
+ },
21
+ content: {
22
+ type: 'string',
23
+ description: '検証したい元の内容(オプション:ハッシュ照合用)'
24
+ }
25
+ },
26
+ required: ['inscription_id']
27
+ },
28
+
29
+ handler: async ({ inscription_id, content }) => {
30
+ const timer = createTimer();
31
+ const network = getNetwork();
32
+ const operator = getOperatorId().toString();
33
+
34
+ // Parse inscription_id to extract topic and timestamp
35
+ // Format: "0.0.XXXXX-TIMESTAMP.NANOS"
36
+ const parts = inscription_id.split('-');
37
+ if (parts.length < 2) {
38
+ logToolExecution({
39
+ command: 'verify',
40
+ network,
41
+ operator,
42
+ status: 'error',
43
+ latency: timer(),
44
+ error: '無効な inscription_id 形式'
45
+ });
46
+ return {
47
+ valid: false,
48
+ error: '無効な inscription_id 形式です',
49
+ inscription_id
50
+ };
51
+ }
52
+
53
+ const topicId = parts[0]; // "0.0.XXXXX"
54
+ const timestamp = parts.slice(1).join('-'); // "TIMESTAMP.NANOS"
55
+
56
+ // Fetch from Mirror Node
57
+ const mirrorUrl = getMirrorNodeUrl();
58
+ const apiUrl = `${mirrorUrl}/api/v1/topics/${topicId}/messages?timestamp=${timestamp}&limit=1`;
59
+
60
+ try {
61
+ const response = await fetch(apiUrl);
62
+ if (!response.ok) {
63
+ logToolExecution({
64
+ command: 'verify',
65
+ network,
66
+ topicId,
67
+ operator,
68
+ status: 'error',
69
+ latency: timer(),
70
+ error: 'Mirror Node fetch failed'
71
+ });
72
+ return {
73
+ valid: false,
74
+ error: 'Mirror Node からの取得に失敗しました',
75
+ inscription_id
76
+ };
77
+ }
78
+
79
+ const data = await response.json();
80
+
81
+ if (!data.messages || data.messages.length === 0) {
82
+ logToolExecution({
83
+ command: 'verify',
84
+ network,
85
+ topicId,
86
+ operator,
87
+ status: 'error',
88
+ latency: timer(),
89
+ error: 'Record not found'
90
+ });
91
+ return {
92
+ valid: false,
93
+ error: '記録が見つかりません',
94
+ inscription_id
95
+ };
96
+ }
97
+
98
+ const message = data.messages[0];
99
+ let inscriptionData;
100
+
101
+ try {
102
+ inscriptionData = JSON.parse(
103
+ Buffer.from(message.message, 'base64').toString('utf-8')
104
+ );
105
+ } catch {
106
+ logToolExecution({
107
+ command: 'verify',
108
+ network,
109
+ topicId,
110
+ operator,
111
+ status: 'error',
112
+ latency: timer(),
113
+ error: 'JSON parse failed'
114
+ });
115
+ return {
116
+ valid: false,
117
+ error: '記録データの解析に失敗しました(JSON形式ではない可能性)',
118
+ inscription_id
119
+ };
120
+ }
121
+
122
+ // Content verification (if provided)
123
+ let contentMatch = null;
124
+ if (content) {
125
+ const providedHash = createHash('sha256').update(content).digest('hex');
126
+ contentMatch = providedHash === inscriptionData.content_hash;
127
+ }
128
+
129
+ // Log success
130
+ logToolExecution({
131
+ command: 'verify',
132
+ network,
133
+ topicId,
134
+ operator,
135
+ status: 'success',
136
+ latency: timer(),
137
+ txId: inscription_id,
138
+ metadata: { contentMatch }
139
+ });
140
+
141
+ return {
142
+ valid: true,
143
+ inscription_id,
144
+ inscribed_at: inscriptionData.inscribed_at || inscriptionData.captured_at,
145
+ consensus_timestamp: message.consensus_timestamp,
146
+ type: inscriptionData.type,
147
+ content_hash: inscriptionData.content_hash,
148
+ content_preview: inscriptionData.content_preview || inscriptionData.content?.substring(0, 100),
149
+ author: inscriptionData.author,
150
+ platform: inscriptionData.platform || null,
151
+ source: inscriptionData.source || null,
152
+ content_match: contentMatch,
153
+ verify_url: `https://hashscan.io/${network}/topic/${topicId}`,
154
+ message: contentMatch === true
155
+ ? '内容が一致しました。改ざんされていません。'
156
+ : contentMatch === false
157
+ ? '警告: 内容が一致しません。改ざんの可能性があります。'
158
+ : '記録が見つかりました。'
159
+ };
160
+ } catch (error) {
161
+ logToolExecution({
162
+ command: 'verify',
163
+ network,
164
+ topicId,
165
+ operator,
166
+ status: 'error',
167
+ latency: timer(),
168
+ error: error.message
169
+ });
170
+ return {
171
+ valid: false,
172
+ error: `検証エラー: ${error.message}`,
173
+ inscription_id
174
+ };
175
+ }
176
+ }
177
+ };