suins-mcp 2.0.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.
Files changed (3) hide show
  1. package/README.md +74 -0
  2. package/index.mjs +344 -0
  3. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # suins-mcp
2
+
3
+ MCP server for Sui Name Service. Lets AI agents resolve `.sui` names, look up identities, and build SuiNS transactions.
4
+
5
+ ## Setup Options
6
+
7
+ ### Option A: npx (no install needed)
8
+ ```bash
9
+ npx suins-mcp
10
+ ```
11
+
12
+ ### Option B: Claude Code
13
+ ```bash
14
+ claude mcp add --transport http suins-mcp https://your-deployed-url/mcp
15
+ ```
16
+
17
+ ### Option C: Claude Desktop / local
18
+ Add to your MCP config:
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "suins": {
23
+ "command": "npx",
24
+ "args": ["-y", "suins-mcp"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Option D: HTTP mode (self-hosted)
31
+ ```bash
32
+ MCP_TRANSPORT=http PORT=3000 node index.mjs
33
+ ```
34
+ Then connect at `http://localhost:3000/mcp`
35
+
36
+ ---
37
+
38
+ ## Tools
39
+
40
+ ### Query tools
41
+ | Tool | Description |
42
+ |---|---|
43
+ | `resolve_name` | Resolve a `.sui` name to a wallet address |
44
+ | `reverse_lookup` | Find the `.sui` name(s) for a wallet address |
45
+ | `get_name_record` | Get full details (expiry, avatar, contentHash) of a name |
46
+ | `check_availability` | Check if a `.sui` name is available to register |
47
+ | `get_pricing` | Get current registration pricing by name length |
48
+ | `get_renewal_pricing` | Get current renewal pricing by name length |
49
+
50
+ ### Transaction tools
51
+ These tools build unsigned Sui PTBs. The returned `txBytes` must be signed and executed by the caller's wallet.
52
+
53
+ | Tool | Description |
54
+ |---|---|
55
+ | `build_register_tx` | Register a new `.sui` name |
56
+ | `build_renew_tx` | Renew an existing `.sui` name |
57
+ | `build_create_subname_tx` | Create a node subname with its own NFT |
58
+ | `build_create_leaf_subname_tx` | Create a leaf subname e.g. `ossy.t2000.sui` |
59
+ | `build_remove_leaf_subname_tx` | Remove a leaf subname |
60
+ | `build_set_target_address_tx` | Point a name at a wallet address |
61
+ | `build_set_default_name_tx` | Set a name as default for the signer address |
62
+ | `build_edit_subname_setup_tx` | Toggle child creation / time extension on a subname |
63
+ | `build_extend_expiration_tx` | Extend a subname expiration |
64
+ | `build_set_metadata_tx` | Set avatar, content hash, or walrus site ID |
65
+ | `build_burn_expired_tx` | Burn expired name to reclaim storage rebates |
66
+
67
+ ---
68
+
69
+ ## Environment Variables
70
+
71
+ | Variable | Default | Description |
72
+ |---|---|---|
73
+ | `MCP_TRANSPORT` | `stdio` | Set to `http` for HTTP mode |
74
+ | `PORT` | `3000` | Port for HTTP mode |
package/index.mjs ADDED
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+ import http from 'node:http';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
+ import { z } from 'zod';
7
+ import { SuiJsonRpcClient, getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc';
8
+ import { SuinsClient, SuinsTransaction, ALLOWED_METADATA } from '@mysten/suins';
9
+ import { Transaction } from '@mysten/sui/transactions';
10
+
11
+ const suiClient = new SuiJsonRpcClient({ url: getJsonRpcFullnodeUrl('mainnet') });
12
+ const suinsClient = new SuinsClient({ client: suiClient, network: 'mainnet' });
13
+
14
+ function createServer() {
15
+ const server = new McpServer({ name: 'suins-mcp', version: '2.0.0' });
16
+
17
+ // ─── QUERY TOOLS ────────────────────────────────────────────────────────
18
+
19
+ server.tool('resolve_name',
20
+ 'Resolve a .sui name to a wallet address',
21
+ { name: z.string().describe('The .sui name to resolve e.g. ossy.sui') },
22
+ async ({ name }) => {
23
+ try {
24
+ const address = await suiClient.resolveNameServiceAddress({ name });
25
+ if (!address) return res({ error: `Name "${name}" not found or has no address` });
26
+ return res({ name, address });
27
+ } catch (e) { return res({ error: e.message }); }
28
+ });
29
+
30
+ server.tool('reverse_lookup',
31
+ 'Find the .sui name(s) for a wallet address',
32
+ { address: z.string().describe('The Sui wallet address to look up') },
33
+ async ({ address }) => {
34
+ try {
35
+ const result = await suiClient.resolveNameServiceNames({ address });
36
+ if (!result?.data?.length) return res({ error: `No .sui name found for "${address}"` });
37
+ return res({ address, names: result.data });
38
+ } catch (e) { return res({ error: e.message }); }
39
+ });
40
+
41
+ server.tool('get_name_record',
42
+ 'Get full details of a .sui name including expiry, avatar, and content hash',
43
+ { name: z.string().describe('The .sui name to get details for') },
44
+ async ({ name }) => {
45
+ try {
46
+ const record = await suinsClient.getNameRecord(name);
47
+ if (!record) return res({ error: `Name "${name}" not found` });
48
+ return res({
49
+ name: record.name,
50
+ address: record.targetAddress,
51
+ expiration: record.expirationTimestampMs,
52
+ avatar: record.avatar,
53
+ contentHash: record.contentHash,
54
+ walrusSiteId: record.walrusSiteId,
55
+ });
56
+ } catch (e) { return res({ error: e.message }); }
57
+ });
58
+
59
+ server.tool('check_availability',
60
+ 'Check if a .sui name is available to register',
61
+ { name: z.string().describe('The .sui name to check e.g. myname.sui') },
62
+ async ({ name }) => {
63
+ try {
64
+ const record = await suinsClient.getNameRecord(name);
65
+ return res({ name, available: !record });
66
+ } catch (e) {
67
+ return res({ name, available: true });
68
+ }
69
+ });
70
+
71
+ server.tool('get_pricing',
72
+ 'Get current SuiNS registration pricing by name length',
73
+ {},
74
+ async () => {
75
+ try {
76
+ const priceList = await suinsClient.getPriceList();
77
+ return res({ prices: Object.fromEntries(priceList), note: 'Prices are in USDC MIST. 1 USDC = 1,000,000 MIST' });
78
+ } catch (e) { return res({ error: e.message }); }
79
+ });
80
+
81
+ server.tool('get_renewal_pricing',
82
+ 'Get current SuiNS renewal pricing by name length',
83
+ {},
84
+ async () => {
85
+ try {
86
+ const priceList = await suinsClient.getRenewalPriceList();
87
+ return res({ prices: Object.fromEntries(priceList), note: 'Prices are in USDC MIST. 1 USDC = 1,000,000 MIST' });
88
+ } catch (e) { return res({ error: e.message }); }
89
+ });
90
+
91
+ // ─── TRANSACTION TOOLS ──────────────────────────────────────────────────
92
+
93
+ server.tool('build_register_tx',
94
+ 'Build a transaction to register a new .sui name. Returns unsigned tx bytes to be signed and executed by the caller.',
95
+ {
96
+ name: z.string().describe('The .sui name to register e.g. myname.sui'),
97
+ years: z.number().min(1).max(5).describe('Number of years to register (1-5)'),
98
+ coin: z.string().describe('Object ID of the coin to pay with'),
99
+ coinType: z.enum(['USDC', 'SUI', 'NS']).describe('Coin type to pay with'),
100
+ recipient: z.string().describe('Address to receive the name NFT'),
101
+ },
102
+ async ({ name, years, coin, coinType, recipient }) => {
103
+ try {
104
+ const tx = new Transaction();
105
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
106
+ const coinConfig = suinsClient.config.coins[coinType];
107
+ let priceInfoObjectId;
108
+ if (coinType !== 'USDC') priceInfoObjectId = (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0];
109
+ const nft = suinsTx.register({ domain: name, years, coinConfig, coin, ...(priceInfoObjectId && { priceInfoObjectId }) });
110
+ tx.transferObjects([nft], tx.pure.address(recipient));
111
+ const txBytes = await tx.build({ client: suiClient });
112
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
113
+ } catch (e) { return res({ error: e.message }); }
114
+ });
115
+
116
+ server.tool('build_renew_tx',
117
+ 'Build a transaction to renew an existing .sui name. Returns unsigned tx bytes.',
118
+ {
119
+ name: z.string().describe('The .sui name to renew'),
120
+ nftId: z.string().describe('Object ID of the SuiNS NFT'),
121
+ years: z.number().min(1).max(5).describe('Number of years to renew (1-5)'),
122
+ coin: z.string().describe('Object ID of the coin to pay with'),
123
+ coinType: z.enum(['USDC', 'SUI', 'NS']).describe('Coin type to pay with'),
124
+ },
125
+ async ({ name, nftId, years, coin, coinType }) => {
126
+ try {
127
+ const tx = new Transaction();
128
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
129
+ const coinConfig = suinsClient.config.coins[coinType];
130
+ let priceInfoObjectId;
131
+ if (coinType !== 'USDC') priceInfoObjectId = (await suinsClient.getPriceInfoObject(tx, coinConfig.feed))[0];
132
+ suinsTx.renew({ nft: nftId, years, coinConfig, coin, ...(priceInfoObjectId && { priceInfoObjectId }) });
133
+ const txBytes = await tx.build({ client: suiClient });
134
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
135
+ } catch (e) { return res({ error: e.message }); }
136
+ });
137
+
138
+ server.tool('build_create_subname_tx',
139
+ 'Build a transaction to create a node subname (has its own NFT). Returns unsigned tx bytes.',
140
+ {
141
+ subname: z.string().describe('Full subname to create e.g. sub.parent.sui'),
142
+ parentNftId: z.string().describe('Object ID of the parent name NFT'),
143
+ expirationMs: z.number().describe('Expiration timestamp in ms (must be <= parent expiration)'),
144
+ recipient: z.string().describe('Address to receive the subname NFT'),
145
+ allowChildCreation: z.boolean().default(true).describe('Whether this subname can create nested subnames'),
146
+ allowTimeExtension: z.boolean().default(true).describe('Whether this subname can extend its expiration'),
147
+ },
148
+ async ({ subname, parentNftId, expirationMs, recipient, allowChildCreation, allowTimeExtension }) => {
149
+ try {
150
+ const tx = new Transaction();
151
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
152
+ const nft = suinsTx.createSubName({ parentNft: parentNftId, name: subname, expirationTimestampMs: expirationMs, allowChildCreation, allowTimeExtension });
153
+ tx.transferObjects([nft], tx.pure.address(recipient));
154
+ const txBytes = await tx.build({ client: suiClient });
155
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
156
+ } catch (e) { return res({ error: e.message }); }
157
+ });
158
+
159
+ server.tool('build_create_leaf_subname_tx',
160
+ 'Build a transaction to create a leaf subname (no NFT, controlled by parent). E.g. ossy.t2000.sui. Returns unsigned tx bytes.',
161
+ {
162
+ subname: z.string().describe('Full leaf subname to create e.g. ossy.t2000.sui'),
163
+ parentNftId: z.string().describe('Object ID of the parent name NFT'),
164
+ targetAddress: z.string().describe('Wallet address this leaf subname should point to'),
165
+ },
166
+ async ({ subname, parentNftId, targetAddress }) => {
167
+ try {
168
+ const tx = new Transaction();
169
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
170
+ suinsTx.createLeafSubName({ parentNft: parentNftId, name: subname, targetAddress });
171
+ const txBytes = await tx.build({ client: suiClient });
172
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
173
+ } catch (e) { return res({ error: e.message }); }
174
+ });
175
+
176
+ server.tool('build_remove_leaf_subname_tx',
177
+ 'Build a transaction to remove a leaf subname. Returns unsigned tx bytes.',
178
+ {
179
+ subname: z.string().describe('Full leaf subname to remove e.g. ossy.t2000.sui'),
180
+ parentNftId: z.string().describe('Object ID of the parent name NFT'),
181
+ },
182
+ async ({ subname, parentNftId }) => {
183
+ try {
184
+ const tx = new Transaction();
185
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
186
+ suinsTx.removeLeafSubName({ parentNft: parentNftId, name: subname });
187
+ const txBytes = await tx.build({ client: suiClient });
188
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
189
+ } catch (e) { return res({ error: e.message }); }
190
+ });
191
+
192
+ server.tool('build_set_target_address_tx',
193
+ 'Build a transaction to set the target address for a .sui name. Returns unsigned tx bytes.',
194
+ {
195
+ nftId: z.string().describe('Object ID of the SuiNS NFT'),
196
+ address: z.string().describe('New target address'),
197
+ isSubname: z.boolean().default(false).describe('Whether this is a subname'),
198
+ },
199
+ async ({ nftId, address, isSubname }) => {
200
+ try {
201
+ const tx = new Transaction();
202
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
203
+ suinsTx.setTargetAddress({ nft: nftId, address, isSubname });
204
+ const txBytes = await tx.build({ client: suiClient });
205
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
206
+ } catch (e) { return res({ error: e.message }); }
207
+ });
208
+
209
+ server.tool('build_set_default_name_tx',
210
+ 'Build a transaction to set a .sui name as the default for the signer address. Returns unsigned tx bytes.',
211
+ { name: z.string().describe('The .sui name to set as default e.g. ossy.sui') },
212
+ async ({ name }) => {
213
+ try {
214
+ const tx = new Transaction();
215
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
216
+ suinsTx.setDefault(name);
217
+ const txBytes = await tx.build({ client: suiClient });
218
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet. Signer must be the target address of this name.' });
219
+ } catch (e) { return res({ error: e.message }); }
220
+ });
221
+
222
+ server.tool('build_edit_subname_setup_tx',
223
+ 'Build a transaction to edit a subname setup (child creation / time extension). Returns unsigned tx bytes.',
224
+ {
225
+ name: z.string().describe('The subname to edit'),
226
+ parentNftId: z.string().describe('Object ID of the parent NFT'),
227
+ allowChildCreation: z.boolean().describe('Whether to allow child subname creation'),
228
+ allowTimeExtension: z.boolean().describe('Whether to allow time extension'),
229
+ },
230
+ async ({ name, parentNftId, allowChildCreation, allowTimeExtension }) => {
231
+ try {
232
+ const tx = new Transaction();
233
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
234
+ suinsTx.editSetup({ name, parentNft: parentNftId, allowChildCreation, allowTimeExtension });
235
+ const txBytes = await tx.build({ client: suiClient });
236
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
237
+ } catch (e) { return res({ error: e.message }); }
238
+ });
239
+
240
+ server.tool('build_extend_expiration_tx',
241
+ 'Build a transaction to extend a subname expiration. Returns unsigned tx bytes.',
242
+ {
243
+ nftId: z.string().describe('Object ID of the subname NFT'),
244
+ expirationMs: z.number().describe('New expiration timestamp in milliseconds'),
245
+ },
246
+ async ({ nftId, expirationMs }) => {
247
+ try {
248
+ const tx = new Transaction();
249
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
250
+ suinsTx.extendExpiration({ nft: nftId, expirationTimestampMs: expirationMs });
251
+ const txBytes = await tx.build({ client: suiClient });
252
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
253
+ } catch (e) { return res({ error: e.message }); }
254
+ });
255
+
256
+ server.tool('build_set_metadata_tx',
257
+ 'Build a transaction to set metadata on a .sui name (avatar, content hash, walrus site ID). Returns unsigned tx bytes.',
258
+ {
259
+ nftId: z.string().describe('Object ID of the SuiNS NFT'),
260
+ isSubname: z.boolean().default(false).describe('Whether this is a subname'),
261
+ avatar: z.string().optional().describe('NFT object ID to use as avatar'),
262
+ contentHash: z.string().optional().describe('IPFS content hash'),
263
+ walrusSiteId: z.string().optional().describe('Walrus site ID'),
264
+ },
265
+ async ({ nftId, isSubname, avatar, contentHash, walrusSiteId }) => {
266
+ try {
267
+ const tx = new Transaction();
268
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
269
+ if (avatar) suinsTx.setUserData({ nft: nftId, key: ALLOWED_METADATA.avatar, value: avatar, isSubname });
270
+ if (contentHash) suinsTx.setUserData({ nft: nftId, key: ALLOWED_METADATA.contentHash, value: contentHash, isSubname });
271
+ if (walrusSiteId) suinsTx.setUserData({ nft: nftId, key: ALLOWED_METADATA.walrusSiteId, value: walrusSiteId, isSubname });
272
+ const txBytes = await tx.build({ client: suiClient });
273
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
274
+ } catch (e) { return res({ error: e.message }); }
275
+ });
276
+
277
+ server.tool('build_burn_expired_tx',
278
+ 'Build a transaction to burn an expired .sui name and reclaim storage rebates. Returns unsigned tx bytes.',
279
+ {
280
+ nftId: z.string().describe('Object ID of the expired SuiNS NFT'),
281
+ isSubname: z.boolean().default(false).describe('Whether this is a subname'),
282
+ },
283
+ async ({ nftId, isSubname }) => {
284
+ try {
285
+ const tx = new Transaction();
286
+ const suinsTx = new SuinsTransaction(suinsClient, tx);
287
+ suinsTx.burnExpired({ nft: nftId, isSubname });
288
+ const txBytes = await tx.build({ client: suiClient });
289
+ return res({ txBytes: Buffer.from(txBytes).toString('base64'), note: 'Sign and execute these tx bytes with your wallet' });
290
+ } catch (e) { return res({ error: e.message }); }
291
+ });
292
+
293
+ return server;
294
+ }
295
+
296
+ function res(data) {
297
+ return { content: [{ type: 'text', text: JSON.stringify(data) }] };
298
+ }
299
+
300
+ // ─── TRANSPORT ──────────────────────────────────────────────────────────────
301
+
302
+ const mode = process.env.MCP_TRANSPORT || 'stdio';
303
+ const port = parseInt(process.env.PORT || '3000');
304
+
305
+ if (mode === 'http') {
306
+ const httpServer = http.createServer(async (req, res) => {
307
+ if (req.method === 'GET' && req.url === '/health') {
308
+ res.writeHead(200, { 'Content-Type': 'application/json' });
309
+ res.end(JSON.stringify({ status: 'ok', name: 'suins-mcp', version: '2.0.0' }));
310
+ return;
311
+ }
312
+
313
+ if (req.url === '/mcp') {
314
+ const server = createServer();
315
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
316
+ await server.connect(transport);
317
+
318
+ let body = '';
319
+ req.on('data', chunk => body += chunk);
320
+ req.on('end', async () => {
321
+ try {
322
+ const parsed = body ? JSON.parse(body) : undefined;
323
+ await transport.handleRequest(req, res, parsed);
324
+ } catch (e) {
325
+ res.writeHead(400);
326
+ res.end(JSON.stringify({ error: e.message }));
327
+ }
328
+ });
329
+ return;
330
+ }
331
+
332
+ res.writeHead(404);
333
+ res.end('Not found');
334
+ });
335
+
336
+ httpServer.listen(port, () => {
337
+ console.log(`suins-mcp HTTP server running on port ${port}`);
338
+ console.log(`MCP endpoint: http://localhost:${port}/mcp`);
339
+ });
340
+ } else {
341
+ const server = createServer();
342
+ const transport = new StdioServerTransport();
343
+ await server.connect(transport);
344
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "suins-mcp",
3
+ "version": "2.0.0",
4
+ "description": "MCP server for Sui Name Service — resolve .sui names, manage identities, and build SuiNS transactions",
5
+ "type": "module",
6
+ "main": "index.mjs",
7
+ "bin": {
8
+ "suins-mcp": "./index.mjs"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.mjs",
12
+ "start:http": "MCP_TRANSPORT=http node index.mjs"
13
+ },
14
+ "keywords": ["sui", "suins", "mcp", "blockchain", "identity", "web3"],
15
+ "author": "ossybadman",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.29.0",
19
+ "@mysten/sui": "^2.14.1",
20
+ "@mysten/suins": "^1.0.2",
21
+ "zod": "^4.3.6"
22
+ }
23
+ }