@hoodag/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.
Files changed (3) hide show
  1. package/README.md +47 -0
  2. package/index.js +133 -0
  3. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @hoodag/mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server for the **.hood** name service on
4
+ [Robinhood Chain](https://hood.ag). It gives an AI agent tools to resolve `.hood` names,
5
+ reverse-resolve addresses, read text records, and check availability & pricing — all as
6
+ read-only on-chain calls (no keys, no writes).
7
+
8
+ ## Run
9
+
10
+ ```bash
11
+ npx @hoodag/mcp
12
+ ```
13
+
14
+ Set `HOOD_RPC_URL` to use your own node (defaults to the public RPC).
15
+
16
+ ## Add to an MCP client
17
+
18
+ **Claude Desktop / Claude Code** (`claude_desktop_config.json` or `.mcp.json`):
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "hood": {
24
+ "command": "npx",
25
+ "args": ["-y", "@hoodag/mcp"]
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ **Cursor** (`.cursor/mcp.json`) uses the same shape.
32
+
33
+ ## Tools
34
+
35
+ | Tool | Input | Returns |
36
+ | --- | --- | --- |
37
+ | `resolve_name` | `name` | address the name points to (or null) |
38
+ | `reverse_resolve` | `address` | primary `.hood` name + `verified` flag |
39
+ | `resolve_text` | `name`, `key` | a text record value (e.g. `com.twitter`) |
40
+ | `check_availability` | `name` | availability + yearly price (wei & USD) |
41
+ | `name_info` | `name` | resolved address, NFT owner, expiry, availability |
42
+
43
+ The `.hood` suffix is optional on every `name` input.
44
+
45
+ ## License
46
+
47
+ MIT
package/index.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @hoodag/mcp — Model Context Protocol server for the .hood name service on Robinhood Chain.
4
+ *
5
+ * Gives an AI agent (Claude, Cursor, …) tools to resolve .hood names, reverse-resolve
6
+ * addresses, read text records, and check availability & pricing.
7
+ *
8
+ * Run: npx @hoodag/mcp
9
+ */
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { z } from 'zod';
13
+ import { createPublicClient, http, keccak256, toBytes, isAddress } from 'viem';
14
+
15
+ const RPC_URL = process.env.HOOD_RPC_URL || 'https://rpc.mainnet.chain.robinhood.com';
16
+ const YEAR = 365n * 24n * 60n * 60n;
17
+
18
+ const ADDR = {
19
+ registrar: '0x9a122C54e15B6287c164b90833bcDA6E16520D50',
20
+ controller: '0x7900b2B8Cc3f0616B18C68AdA7E115A9022Ca04a',
21
+ universal: '0xa9ed3C73F522875F18DAa631429125da893D8E53',
22
+ };
23
+
24
+ const universalAbi = [
25
+ { type: 'function', name: 'resolve', stateMutability: 'view', inputs: [{ name: 'name', type: 'string' }], outputs: [{ type: 'address' }] },
26
+ { type: 'function', name: 'reverse', stateMutability: 'view', inputs: [{ name: 'a', type: 'address' }], outputs: [{ name: 'name', type: 'string' }, { name: 'verified', type: 'bool' }] },
27
+ { type: 'function', name: 'resolveText', stateMutability: 'view', inputs: [{ name: 'name', type: 'string' }, { name: 'key', type: 'string' }], outputs: [{ type: 'string' }] },
28
+ ];
29
+ const controllerAbi = [
30
+ { type: 'function', name: 'available', stateMutability: 'view', inputs: [{ name: 'nm', type: 'string' }], outputs: [{ type: 'bool' }] },
31
+ { type: 'function', name: 'price', stateMutability: 'view', inputs: [{ name: 'nm', type: 'string' }, { name: 'dur', type: 'uint256' }], outputs: [{ type: 'uint256' }] },
32
+ { type: 'function', name: 'ethUsd', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
33
+ ];
34
+ const registrarAbi = [
35
+ { type: 'function', name: 'ownerOf', stateMutability: 'view', inputs: [{ name: 'id', type: 'uint256' }], outputs: [{ type: 'address' }] },
36
+ { type: 'function', name: 'nameExpires', stateMutability: 'view', inputs: [{ name: 'id', type: 'uint256' }], outputs: [{ type: 'uint256' }] },
37
+ ];
38
+
39
+ const client = createPublicClient({ transport: http(RPC_URL) });
40
+
41
+ const ZERO = '0x0000000000000000000000000000000000000000';
42
+ const label = (name) => name.toLowerCase().replace(/\.hood$/, '');
43
+ const fqdn = (name) => (name.toLowerCase().endsWith('.hood') ? name.toLowerCase() : `${label(name)}.hood`);
44
+ const tokenId = (name) => BigInt(keccak256(toBytes(label(name))));
45
+ const usd = (wei, ethUsd) => (!wei || !ethUsd ? null : (Number(wei) / 1e18) * (Number(ethUsd) / 1e18));
46
+ const text = (obj) => ({ content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] });
47
+ const fail = (msg) => ({ isError: true, content: [{ type: 'text', text: msg }] });
48
+
49
+ const server = new McpServer({ name: 'hood', version: '0.1.0' });
50
+
51
+ server.tool(
52
+ 'resolve_name',
53
+ 'Forward-resolve a .hood name to the address it points to. Returns null if unset.',
54
+ { name: z.string().describe('A .hood name, e.g. "robin.hood" (the .hood suffix is optional).') },
55
+ async ({ name }) => {
56
+ try {
57
+ const addr = await client.readContract({ address: ADDR.universal, abi: universalAbi, functionName: 'resolve', args: [fqdn(name)] });
58
+ return text({ name: fqdn(name), address: addr === ZERO ? null : addr });
59
+ } catch (e) { return fail(`resolve failed: ${e.shortMessage || e.message}`); }
60
+ },
61
+ );
62
+
63
+ server.tool(
64
+ 'reverse_resolve',
65
+ 'Reverse-resolve an address to its primary .hood name. `verified` is true only when the name\'s forward record points back to the address.',
66
+ { address: z.string().describe('A 0x wallet address.') },
67
+ async ({ address }) => {
68
+ if (!isAddress(address)) return fail('not a valid address');
69
+ try {
70
+ const [name, verified] = await client.readContract({ address: ADDR.universal, abi: universalAbi, functionName: 'reverse', args: [address] });
71
+ return text({ address, name: name || null, verified });
72
+ } catch (e) { return fail(`reverse failed: ${e.shortMessage || e.message}`); }
73
+ },
74
+ );
75
+
76
+ server.tool(
77
+ 'resolve_text',
78
+ 'Read a text record (e.g. "avatar", "url", "com.twitter") set on a .hood name.',
79
+ { name: z.string().describe('A .hood name.'), key: z.string().describe('Text record key, e.g. "com.twitter".') },
80
+ async ({ name, key }) => {
81
+ try {
82
+ const value = await client.readContract({ address: ADDR.universal, abi: universalAbi, functionName: 'resolveText', args: [fqdn(name), key] });
83
+ return text({ name: fqdn(name), key, value: value || null });
84
+ } catch (e) { return fail(`resolveText failed: ${e.shortMessage || e.message}`); }
85
+ },
86
+ );
87
+
88
+ server.tool(
89
+ 'check_availability',
90
+ 'Check whether a .hood name is available to register, and its yearly price.',
91
+ { name: z.string().describe('The name to check (with or without .hood).') },
92
+ async ({ name }) => {
93
+ const nm = label(name);
94
+ try {
95
+ const [available, priceWei, ethUsd] = await Promise.all([
96
+ client.readContract({ address: ADDR.controller, abi: controllerAbi, functionName: 'available', args: [nm] }),
97
+ client.readContract({ address: ADDR.controller, abi: controllerAbi, functionName: 'price', args: [nm, YEAR] }).catch(() => null),
98
+ client.readContract({ address: ADDR.controller, abi: controllerAbi, functionName: 'ethUsd', args: [] }).catch(() => null),
99
+ ]);
100
+ const priceUsd = usd(priceWei, ethUsd);
101
+ return text({ name: `${nm}.hood`, available, priceWeiPerYear: priceWei ? priceWei.toString() : null, priceUsdPerYear: priceUsd ? Math.round(priceUsd * 100) / 100 : null });
102
+ } catch (e) { return fail(`availability check failed: ${e.shortMessage || e.message}`); }
103
+ },
104
+ );
105
+
106
+ server.tool(
107
+ 'name_info',
108
+ 'Full snapshot of a .hood name: the address it resolves to, the NFT owner, expiry, and availability.',
109
+ { name: z.string().describe('A .hood name.') },
110
+ async ({ name }) => {
111
+ const nm = label(name);
112
+ const id = tokenId(name);
113
+ try {
114
+ const [addr, available, owner, expires] = await Promise.all([
115
+ client.readContract({ address: ADDR.universal, abi: universalAbi, functionName: 'resolve', args: [`${nm}.hood`] }).catch(() => ZERO),
116
+ client.readContract({ address: ADDR.controller, abi: controllerAbi, functionName: 'available', args: [nm] }).catch(() => null),
117
+ client.readContract({ address: ADDR.registrar, abi: registrarAbi, functionName: 'ownerOf', args: [id] }).catch(() => null),
118
+ client.readContract({ address: ADDR.registrar, abi: registrarAbi, functionName: 'nameExpires', args: [id] }).catch(() => null),
119
+ ]);
120
+ return text({
121
+ name: `${nm}.hood`,
122
+ resolvesTo: addr === ZERO ? null : addr,
123
+ nftOwner: owner,
124
+ expires: expires ? new Date(Number(expires) * 1000).toISOString() : null,
125
+ available,
126
+ });
127
+ } catch (e) { return fail(`name_info failed: ${e.shortMessage || e.message}`); }
128
+ },
129
+ );
130
+
131
+ const transport = new StdioServerTransport();
132
+ await server.connect(transport);
133
+ console.error(`[hood-mcp] connected · RPC ${RPC_URL}`);
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@hoodag/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the .hood name service on Robinhood Chain — lets AI agents resolve names, reverse-resolve addresses, read records, and check availability.",
5
+ "type": "module",
6
+ "bin": {
7
+ "hood-mcp": "./index.js"
8
+ },
9
+ "main": "./index.js",
10
+ "files": [
11
+ "index.js",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "hood",
21
+ "ens",
22
+ "robinhood-chain",
23
+ "name-service",
24
+ "ai-agents",
25
+ "web3"
26
+ ],
27
+ "homepage": "https://hood.ag",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.12.0",
31
+ "viem": "^2.31.0",
32
+ "zod": "^3.23.8"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }