@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,239 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * inscribe-mcp Server (SSE/HTTP transport)
4
+ *
5
+ * MCP Server for verifiable inscriptions on Hedera blockchain.
6
+ * For remote access from Windows Claude Desktop via mcp-remote.
7
+ */
8
+
9
+ import express from 'express';
10
+ import cors from 'cors';
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
13
+ import {
14
+ CallToolRequestSchema,
15
+ ListToolsRequestSchema,
16
+ } from '@modelcontextprotocol/sdk/types.js';
17
+ import dotenv from 'dotenv';
18
+
19
+ // Layer 1 Tools (Primary)
20
+ import { inscribeUrlTool } from './tools/layer1/inscribe_url.js';
21
+ import { inscribeTool } from './tools/layer1/inscribe.js';
22
+ import { verifyTool } from './tools/layer1/verify.js';
23
+ import { historyTool } from './tools/layer1/history.js';
24
+ import { identityTool } from './tools/layer1/identity.js';
25
+
26
+ // Layer 2 Tools (Advanced)
27
+ import { createTopicTool, submitMessageTool, getMessagesTool } from './tools/layer2/hcs.js';
28
+ import { getBalanceTool, sendHbarTool, getAccountInfoTool } from './tools/layer2/account.js';
29
+
30
+ dotenv.config();
31
+
32
+ const PORT = process.env.PORT || 3011;
33
+ const HOST = process.env.HOST || '0.0.0.0';
34
+
35
+ // All available tools
36
+ const allTools = [
37
+ // Layer 1 (Primary - Recommended)
38
+ inscribeUrlTool,
39
+ inscribeTool,
40
+ verifyTool,
41
+ historyTool,
42
+ identityTool,
43
+ // Layer 2 (Advanced - For developers)
44
+ createTopicTool,
45
+ submitMessageTool,
46
+ getMessagesTool,
47
+ getBalanceTool,
48
+ sendHbarTool,
49
+ getAccountInfoTool,
50
+ ];
51
+
52
+ // Create MCP Server factory
53
+ function createMcpServer() {
54
+ const server = new Server(
55
+ {
56
+ name: 'inscribe-mcp',
57
+ version: '1.0.0',
58
+ },
59
+ {
60
+ capabilities: {
61
+ tools: {},
62
+ },
63
+ }
64
+ );
65
+
66
+ // List Tools Handler
67
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
68
+ return {
69
+ tools: allTools.map(t => ({
70
+ name: t.name,
71
+ description: t.description,
72
+ inputSchema: t.inputSchema
73
+ }))
74
+ };
75
+ });
76
+
77
+ // Call Tool Handler
78
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
79
+ const { name, arguments: args } = request.params;
80
+ const tool = allTools.find(t => t.name === name);
81
+
82
+ if (!tool) {
83
+ return {
84
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
85
+ isError: true
86
+ };
87
+ }
88
+
89
+ try {
90
+ const result = await tool.handler(args || {});
91
+ return {
92
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
93
+ };
94
+ } catch (error) {
95
+ return {
96
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
97
+ isError: true
98
+ };
99
+ }
100
+ });
101
+
102
+ return server;
103
+ }
104
+
105
+ // Express app
106
+ const app = express();
107
+ app.use(cors());
108
+ app.use(express.json());
109
+
110
+ // Track transports for each session
111
+ const transports = {};
112
+
113
+ // Health check
114
+ app.get('/health', (req, res) => {
115
+ res.json({
116
+ status: 'ok',
117
+ server: 'inscribe-mcp',
118
+ version: '1.0.0',
119
+ transport: 'sse',
120
+ connections: Object.keys(transports).length,
121
+ tools: allTools.length
122
+ });
123
+ });
124
+
125
+ // SSE endpoint
126
+ app.get('/sse', async (req, res) => {
127
+ console.log(`[SSE] New connection from ${req.ip}`);
128
+
129
+ const transport = new SSEServerTransport('/messages', res);
130
+ transports[transport.sessionId] = transport;
131
+
132
+ res.on('close', () => {
133
+ console.log(`[SSE] Connection closed: ${transport.sessionId}`);
134
+ delete transports[transport.sessionId];
135
+ });
136
+
137
+ const server = createMcpServer();
138
+ await server.connect(transport);
139
+ console.log(`[SSE] Server connected: ${transport.sessionId}`);
140
+ });
141
+
142
+ // Messages endpoint
143
+ app.post('/messages', async (req, res) => {
144
+ const sessionId = req.query.sessionId;
145
+ console.log(`[Messages] Received for session: ${sessionId}`);
146
+
147
+ const transport = transports[sessionId];
148
+
149
+ if (!transport) {
150
+ console.log(`[Messages] Session not found: ${sessionId}`);
151
+ res.status(400).json({ error: 'Session not found' });
152
+ return;
153
+ }
154
+
155
+ try {
156
+ await transport.handlePostMessage(req, res, req.body);
157
+ } catch (error) {
158
+ console.error(`[Messages] Error: ${error.message}`);
159
+ if (!res.headersSent) {
160
+ res.status(500).json({ error: error.message });
161
+ }
162
+ }
163
+ });
164
+
165
+ // Info page
166
+ app.get('/', (req, res) => {
167
+ res.send(`
168
+ <!DOCTYPE html>
169
+ <html>
170
+ <head>
171
+ <title>inscribe-mcp</title>
172
+ <style>
173
+ body { font-family: system-ui; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
174
+ h1 { color: #333; }
175
+ code { background: #f4f4f4; padding: 0.2rem 0.5rem; border-radius: 4px; }
176
+ pre { background: #f4f4f4; padding: 1rem; border-radius: 8px; overflow-x: auto; }
177
+ .tool { margin: 0.5rem 0; padding: 0.5rem; background: #f9f9f9; border-radius: 4px; }
178
+ .layer1 { border-left: 3px solid #4CAF50; }
179
+ .layer2 { border-left: 3px solid #FF9800; }
180
+ </style>
181
+ </head>
182
+ <body>
183
+ <h1>inscribe-mcp</h1>
184
+ <p>Verifiable inscription for AI agents</p>
185
+
186
+ <h2>Connection</h2>
187
+ <p>SSE Endpoint: <code>http://${req.headers.host}/sse</code></p>
188
+
189
+ <h3>Claude Desktop (Windows)</h3>
190
+ <pre>{
191
+ "mcpServers": {
192
+ "inscribe": {
193
+ "command": "npx",
194
+ "args": ["-y", "mcp-remote", "http://${req.headers.host}/sse", "--allow-http"],
195
+ "env": {}
196
+ }
197
+ }
198
+ }</pre>
199
+
200
+ <h2>Layer 1 Tools (Recommended)</h2>
201
+ ${allTools.filter(t => !t.name.startsWith('hcs_') && !t.name.startsWith('account_')).map(t => `
202
+ <div class="tool layer1">
203
+ <strong>${t.name}</strong><br>
204
+ <small>${t.description}</small>
205
+ </div>
206
+ `).join('')}
207
+
208
+ <h2>Layer 2 Tools (Advanced)</h2>
209
+ ${allTools.filter(t => t.name.startsWith('hcs_') || t.name.startsWith('account_')).map(t => `
210
+ <div class="tool layer2">
211
+ <strong>${t.name}</strong><br>
212
+ <small>${t.description}</small>
213
+ </div>
214
+ `).join('')}
215
+
216
+ <h2>Links</h2>
217
+ <ul>
218
+ <li><a href="/health">Health Check</a></li>
219
+ <li><a href="https://github.com/Shin-R2un/inscribe-mcp">GitHub</a></li>
220
+ </ul>
221
+ </body>
222
+ </html>
223
+ `);
224
+ });
225
+
226
+ // Start server
227
+ app.listen(PORT, HOST, () => {
228
+ console.log(`
229
+ ╔═══════════════════════════════════════════════════════════╗
230
+ ║ inscribe-mcp Server (SSE Transport) ║
231
+ ╠═══════════════════════════════════════════════════════════╣
232
+ ║ Server: http://${HOST}:${PORT} ║
233
+ ║ SSE Endpoint: http://${HOST}:${PORT}/sse ║
234
+ ║ Health Check: http://${HOST}:${PORT}/health ║
235
+ ╠═══════════════════════════════════════════════════════════╣
236
+ ║ Tools: ${allTools.length} (Layer 1: 5, Layer 2: 6) ║
237
+ ╚═══════════════════════════════════════════════════════════╝
238
+ `);
239
+ });
package/src/server.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * inscribe-mcp Server (stdio transport)
4
+ *
5
+ * MCP Server for verifiable inscriptions on Hedera blockchain.
6
+ */
7
+
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import {
11
+ CallToolRequestSchema,
12
+ ListToolsRequestSchema,
13
+ } from '@modelcontextprotocol/sdk/types.js';
14
+ import dotenv from 'dotenv';
15
+
16
+ // Layer 1 Tools (Primary)
17
+ import { inscribeUrlTool } from './tools/layer1/inscribe_url.js';
18
+ import { inscribeTool } from './tools/layer1/inscribe.js';
19
+ import { verifyTool } from './tools/layer1/verify.js';
20
+ import { historyTool } from './tools/layer1/history.js';
21
+ import { identityTool } from './tools/layer1/identity.js';
22
+
23
+ // Layer 2 Tools (Advanced)
24
+ import { createTopicTool, submitMessageTool, getMessagesTool } from './tools/layer2/hcs.js';
25
+ import { getBalanceTool, sendHbarTool, getAccountInfoTool } from './tools/layer2/account.js';
26
+
27
+ dotenv.config();
28
+
29
+ // All available tools
30
+ const allTools = [
31
+ // Layer 1 (Primary - Recommended)
32
+ inscribeUrlTool, // Killer feature
33
+ inscribeTool,
34
+ verifyTool,
35
+ historyTool,
36
+ identityTool,
37
+ // Layer 2 (Advanced - For developers)
38
+ createTopicTool,
39
+ submitMessageTool,
40
+ getMessagesTool,
41
+ getBalanceTool,
42
+ sendHbarTool,
43
+ getAccountInfoTool,
44
+ ];
45
+
46
+ // Create MCP Server
47
+ const server = new Server(
48
+ {
49
+ name: 'inscribe-mcp',
50
+ version: '1.0.0',
51
+ },
52
+ {
53
+ capabilities: {
54
+ tools: {},
55
+ },
56
+ }
57
+ );
58
+
59
+ // List Tools Handler
60
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
61
+ return {
62
+ tools: allTools.map(t => ({
63
+ name: t.name,
64
+ description: t.description,
65
+ inputSchema: t.inputSchema
66
+ }))
67
+ };
68
+ });
69
+
70
+ // Call Tool Handler
71
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
72
+ const { name, arguments: args } = request.params;
73
+ const tool = allTools.find(t => t.name === name);
74
+
75
+ if (!tool) {
76
+ return {
77
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
78
+ isError: true
79
+ };
80
+ }
81
+
82
+ try {
83
+ const result = await tool.handler(args || {});
84
+ return {
85
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
86
+ };
87
+ } catch (error) {
88
+ return {
89
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
90
+ isError: true
91
+ };
92
+ }
93
+ });
94
+
95
+ // Main
96
+ async function main() {
97
+ const transport = new StdioServerTransport();
98
+ await server.connect(transport);
99
+ console.error('inscribe-mcp server started (stdio)');
100
+ }
101
+
102
+ main().catch(console.error);
package/src/test.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * inscribe-mcp Test Script
4
+ *
5
+ * Direct tool testing without MCP client
6
+ */
7
+
8
+ import dotenv from 'dotenv';
9
+ dotenv.config();
10
+
11
+ // Layer 1 Tools
12
+ import { inscribeUrlTool } from './tools/layer1/inscribe_url.js';
13
+ import { inscribeTool } from './tools/layer1/inscribe.js';
14
+ import { verifyTool } from './tools/layer1/verify.js';
15
+ import { historyTool } from './tools/layer1/history.js';
16
+
17
+ // Layer 2 Tools
18
+ import { getBalanceTool } from './tools/layer2/account.js';
19
+
20
+ async function testMcpServer() {
21
+ console.log('=== inscribe-mcp Test ===\n');
22
+
23
+ const allTools = [
24
+ inscribeUrlTool, inscribeTool, verifyTool, historyTool, getBalanceTool
25
+ ];
26
+
27
+ try {
28
+ console.log('✅ Tools loaded\n');
29
+
30
+ // 1. List Tools
31
+ console.log('1. List Tools:');
32
+ console.log('---');
33
+ allTools.forEach(t => console.log(` • ${t.name}: ${t.description.substring(0, 50)}...`));
34
+ console.log(`\n Total: ${allTools.length} tools (testing subset)\n`);
35
+
36
+ // 2. Test account_get_balance
37
+ console.log('2. Test account_get_balance:');
38
+ console.log('---');
39
+ const balanceData = await getBalanceTool.handler({});
40
+ console.log(` Account: ${balanceData.account_id}`);
41
+ console.log(` Balance: ${balanceData.balance_hbar} HBAR\n`);
42
+
43
+ // 3. Test inscribe
44
+ console.log('3. Test inscribe:');
45
+ console.log('---');
46
+ const inscribeData = await inscribeTool.handler({
47
+ content: 'inscribe-mcp テスト: ' + new Date().toISOString(),
48
+ type: 'test'
49
+ });
50
+ console.log(` Success: ${inscribeData.success}`);
51
+ console.log(` Inscription ID: ${inscribeData.inscription_id}`);
52
+ console.log(` Hash: ${inscribeData.content_hash?.substring(0, 16)}...`);
53
+ console.log(` HashScan: ${inscribeData.verify_url}\n`);
54
+
55
+ // 4. Test verify
56
+ if (inscribeData.success && inscribeData.inscription_id) {
57
+ // Wait for mirror node to index
58
+ console.log('4. Test verify (waiting 3s for indexing):');
59
+ console.log('---');
60
+ await new Promise(r => setTimeout(r, 3000));
61
+
62
+ const verifyData = await verifyTool.handler({
63
+ inscription_id: inscribeData.inscription_id
64
+ });
65
+ console.log(` Valid: ${verifyData.valid}`);
66
+ console.log(` Type: ${verifyData.type}`);
67
+ console.log(` Timestamp: ${verifyData.consensus_timestamp}\n`);
68
+ }
69
+
70
+ // 5. Test history
71
+ console.log('5. Test history:');
72
+ console.log('---');
73
+ const historyData = await historyTool.handler({ limit: 5 });
74
+ console.log(` Success: ${historyData.success}`);
75
+ console.log(` Count: ${historyData.count}`);
76
+ if (historyData.inscriptions?.length > 0) {
77
+ console.log(` Latest: ${historyData.inscriptions[0].type} - ${historyData.inscriptions[0].content_preview?.substring(0, 40)}...`);
78
+ }
79
+ console.log('');
80
+
81
+ // 6. Test inscribe_url (YouTube)
82
+ console.log('6. Test inscribe_url (YouTube):');
83
+ console.log('---');
84
+ const urlData = await inscribeUrlTool.handler({
85
+ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
86
+ note: 'テスト用'
87
+ });
88
+ console.log(` Success: ${urlData.success}`);
89
+ if (urlData.success) {
90
+ console.log(` Platform: ${urlData.capture?.platform}`);
91
+ console.log(` Source: ${urlData.capture?.source}`);
92
+ console.log(` Author: ${urlData.capture?.author}`);
93
+ console.log(` Content: ${urlData.capture?.content_preview?.substring(0, 50)}...`);
94
+ } else {
95
+ console.log(` Error: ${urlData.error}`);
96
+ }
97
+ console.log('');
98
+
99
+ console.log('=== All Tests Completed! ===');
100
+
101
+ } catch (error) {
102
+ console.error('Test error:', error.message);
103
+ console.error(error.stack);
104
+ }
105
+ }
106
+
107
+ testMcpServer().catch(console.error);
@@ -0,0 +1,174 @@
1
+ /**
2
+ * history - Inscription History Tool
3
+ *
4
+ * Retrieve history of inscriptions.
5
+ */
6
+
7
+ import { getMirrorNodeUrl, getNetwork, getOperatorId } from '../../lib/hedera.js';
8
+ import { logToolExecution, createTimer } from '../../lib/logger.js';
9
+
10
+ export const historyTool = {
11
+ name: 'history',
12
+ description: '刻んだ履歴を取得する',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ limit: {
17
+ type: 'number',
18
+ description: '取得件数(デフォルト: 10、最大: 100)',
19
+ default: 10
20
+ },
21
+ type: {
22
+ type: 'string',
23
+ description: '種類でフィルター(article, log, decision, url_capture など)'
24
+ },
25
+ author: {
26
+ type: 'string',
27
+ description: '著者でフィルター'
28
+ },
29
+ topic_id: {
30
+ type: 'string',
31
+ description: 'トピックID(省略時はデフォルトトピック)'
32
+ }
33
+ }
34
+ },
35
+
36
+ handler: async ({ limit = 10, type, author, topic_id }) => {
37
+ const timer = createTimer();
38
+ const network = getNetwork();
39
+ const operator = getOperatorId().toString();
40
+ const topicId = topic_id || process.env.DEFAULT_TOPIC_ID;
41
+
42
+ if (!topicId) {
43
+ logToolExecution({
44
+ command: 'history',
45
+ network,
46
+ operator,
47
+ status: 'error',
48
+ latency: timer(),
49
+ error: 'Topic ID not specified'
50
+ });
51
+ return {
52
+ success: false,
53
+ error: 'トピックIDが指定されていません',
54
+ inscriptions: [],
55
+ count: 0
56
+ };
57
+ }
58
+
59
+ // Limit to max 100
60
+ const fetchLimit = Math.min(limit, 100);
61
+
62
+ // Fetch from Mirror Node
63
+ const mirrorUrl = getMirrorNodeUrl();
64
+ const apiUrl = `${mirrorUrl}/api/v1/topics/${topicId}/messages?limit=${fetchLimit}&order=desc`;
65
+
66
+ try {
67
+ const response = await fetch(apiUrl);
68
+ if (!response.ok) {
69
+ logToolExecution({
70
+ command: 'history',
71
+ network,
72
+ topicId,
73
+ operator,
74
+ status: 'error',
75
+ latency: timer(),
76
+ error: 'Mirror Node fetch failed'
77
+ });
78
+ return {
79
+ success: false,
80
+ error: 'Mirror Node からの取得に失敗しました',
81
+ inscriptions: [],
82
+ count: 0
83
+ };
84
+ }
85
+
86
+ const data = await response.json();
87
+
88
+ if (!data.messages) {
89
+ logToolExecution({
90
+ command: 'history',
91
+ network,
92
+ topicId,
93
+ operator,
94
+ status: 'success',
95
+ latency: timer(),
96
+ metadata: { count: 0 }
97
+ });
98
+ return {
99
+ success: true,
100
+ inscriptions: [],
101
+ count: 0,
102
+ topic_id: topicId
103
+ };
104
+ }
105
+
106
+ // Parse and filter inscriptions
107
+ let inscriptions = data.messages.map(msg => {
108
+ try {
109
+ const content = JSON.parse(
110
+ Buffer.from(msg.message, 'base64').toString('utf-8')
111
+ );
112
+ return {
113
+ inscription_id: `${topicId}-${msg.consensus_timestamp}`,
114
+ consensus_timestamp: msg.consensus_timestamp,
115
+ ...content
116
+ };
117
+ } catch {
118
+ // Skip non-JSON messages
119
+ return null;
120
+ }
121
+ }).filter(Boolean);
122
+
123
+ // Apply filters
124
+ if (type) {
125
+ inscriptions = inscriptions.filter(i => i.type === type);
126
+ }
127
+ if (author) {
128
+ inscriptions = inscriptions.filter(i => i.author === author);
129
+ }
130
+
131
+ logToolExecution({
132
+ command: 'history',
133
+ network,
134
+ topicId,
135
+ operator,
136
+ status: 'success',
137
+ latency: timer(),
138
+ metadata: { count: inscriptions.length }
139
+ });
140
+
141
+ return {
142
+ success: true,
143
+ inscriptions: inscriptions.map(i => ({
144
+ inscription_id: i.inscription_id,
145
+ type: i.type,
146
+ content_preview: i.content_preview || i.content?.substring(0, 100),
147
+ author: i.author,
148
+ platform: i.platform,
149
+ inscribed_at: i.inscribed_at || i.captured_at,
150
+ consensus_timestamp: i.consensus_timestamp
151
+ })),
152
+ count: inscriptions.length,
153
+ topic_id: topicId,
154
+ verify_url: `https://hashscan.io/${network}/topic/${topicId}`
155
+ };
156
+ } catch (error) {
157
+ logToolExecution({
158
+ command: 'history',
159
+ network,
160
+ topicId,
161
+ operator,
162
+ status: 'error',
163
+ latency: timer(),
164
+ error: error.message
165
+ });
166
+ return {
167
+ success: false,
168
+ error: `履歴取得エラー: ${error.message}`,
169
+ inscriptions: [],
170
+ count: 0
171
+ };
172
+ }
173
+ }
174
+ };