nano-currency-mcp-server 1.0.1
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/Dockerfile +14 -0
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/assets/claude-desktop-edit-config-button.png +0 -0
- package/assets/claude-desktop-settings-in-menu.png +0 -0
- package/assets/claude-desktop-tool-descriptions.png +0 -0
- package/assets/claude-desktop-tools-installed.png +0 -0
- package/nano-currency.js +446 -0
- package/package.json +41 -0
- package/smithery.yaml +41 -0
package/Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
|
|
2
|
+
FROM node:lts-alpine
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# install only production dependencies
|
|
7
|
+
COPY package*.json ./
|
|
8
|
+
RUN npm install --production
|
|
9
|
+
|
|
10
|
+
# copy source
|
|
11
|
+
COPY . .
|
|
12
|
+
|
|
13
|
+
# default command
|
|
14
|
+
CMD ["node", "nano-currency.js"]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Frank Kilkelly
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Nano Currency MCP Server
|
|
2
|
+
[](https://smithery.ai/server/@kilkelly/nano-currency-mcp-server)
|
|
3
|
+
|
|
4
|
+
This Model Context Protocol (MCP) server gives MCP-compatible clients (which include some AI agents) the ability to send Nano currency and retrieve account & block information via the Nano node RPC.
|
|
5
|
+
|
|
6
|
+
<a href="https://glama.ai/mcp/servers/@kilkelly/nano-currency-mcp-server">
|
|
7
|
+
<img width="380" height="200" src="https://glama.ai/mcp/servers/@kilkelly/nano-currency-mcp-server/badge" alt="Nano Currency Server MCP server" />
|
|
8
|
+
</a>
|
|
9
|
+
|
|
10
|
+
AI agents are increasingly adopting the MCP standard so this server can give them the ability to send Nano at their owner's request or possibility autonomously in some setups π€
|
|
11
|
+
|
|
12
|
+
## β What is MCP?
|
|
13
|
+
|
|
14
|
+
The Model Context Protocol (MCP) is an open framework that defines a standardized approach for applications to deliver context to Large Language Models (LLMs).
|
|
15
|
+
|
|
16
|
+
## β What is Nano Currency?
|
|
17
|
+
|
|
18
|
+
Nano is a digital currency designed to enable fast, scalable, and feeless transactions. It aims to address common issues in traditional cryptocurrencies, such as high fees and slow processing times, making it an efficient option for everyday peer-to-peer payments. Learn more at [nano.org](https://nano.org)
|
|
19
|
+
|
|
20
|
+
## π¨ Before Proceeding π¨
|
|
21
|
+
|
|
22
|
+
Caution: LLMs can hallucinate and not always perform as you want so test this server with small amounts of Nano.
|
|
23
|
+
|
|
24
|
+
## π οΈ Tools Provided by the MCP Server
|
|
25
|
+
|
|
26
|
+
π§ `nano_send` - Sends a specified amount of Nano currency
|
|
27
|
+
|
|
28
|
+
π§ `nano_account_info` - Retrieves detailed information about a specific Nano account/address
|
|
29
|
+
|
|
30
|
+
π§ `nano_my_account_info` - Retrieves detailed information about your predefined Nano account/address
|
|
31
|
+
|
|
32
|
+
π§ `block_info` - Retrieves detailed information about a specific Nano block
|
|
33
|
+
|
|
34
|
+
## Prerequisites
|
|
35
|
+
|
|
36
|
+
Make sure you have [Node.js](https://nodejs.org/) with NPM installed.
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
git clone https://github.com/kilkelly/nano-currency-mcp-server.git
|
|
42
|
+
cd nano-currency-mcp-server
|
|
43
|
+
npm install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
You will need an MCP client to connect to the MCP server (see the Claude Desktop setup later as an example MCP client). Each client will have its own way to connect to MCP servers. For your chosen client you will have to find out how environment variables for a MCP server are set. When you know how you will need to set the following environment variables to use the Nano Currency MCP Server.
|
|
48
|
+
|
|
49
|
+
### Environment Variables
|
|
50
|
+
|
|
51
|
+
`NANO_RPC_URL` - URL which should be used to communicate with a Nano node RPC. This can be a local or remotely hosted endpoint.
|
|
52
|
+
This URL value is **required**.
|
|
53
|
+
|
|
54
|
+
`NANO_WORK_GENERATION_URL` - URL which should be used to communicate with an endpoint that supports the [work_generate](https://docs.nano.org/commands/rpc-protocol/#work_generate) RPC command for work generation. If not specified, defaults to `NANO_RPC_URL`. Used by tool π§ `nano_send`
|
|
55
|
+
|
|
56
|
+
`NANO_PRIVATE_KEY` - Nano private key which will be used to sign send transactions and to derive the Nano address from. Caution: π¨*NOT THE WALLET SEED*π¨. Test with the private key of an account with a small Nano balance. Used by tools π§ `nano_send` and π§ `nano_my_account_info`
|
|
57
|
+
|
|
58
|
+
`NANO_MAX_SEND_AMOUNT` - Maximum amount (in nano/ΣΎ units) which can be sent in a single transaction. For safety purposes the default maximum send amount is 0.01 nano (ΣΎ0.01). You must set this variable explicitly to grant the power to send higher amounts. Used by tools: π§ `nano_send`
|
|
59
|
+
|
|
60
|
+
## Claude Desktop Setup
|
|
61
|
+
|
|
62
|
+
### 1. Install and run [Claude Desktop](https://claude.ai/download)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
### 2. Open the Settings menu
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
### 3. Click the `Developer` tab and then `Edit Config` button to open the location of the Claude config file `claude_desktop_config.json`
|
|
71
|
+
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### 4. Open up `claude_desktop_config.json` in your text editor of choice and enter the following but swapping out the values for your unique configuration:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"nano_currency": {
|
|
81
|
+
"command": "ENTER_FULL_FILE_PATH_TO_NODE_DOT_EXE_ON_YOUR_SYSTEM",
|
|
82
|
+
"args": [
|
|
83
|
+
"ENTER_FULL_FILE_PATH_TO_NANO_CURRENCY_JS_FILE_FROM_THIS_REPOSITORY"
|
|
84
|
+
],
|
|
85
|
+
"env": {
|
|
86
|
+
"NANO_RPC_URL": "ENTER_YOUR_NANO_RPC_URL",
|
|
87
|
+
"NANO_WORK_GENERATION_URL": "ENTER_YOUR_NANO_WORK_GENERATION_URL",
|
|
88
|
+
"NANO_PRIVATE_KEY": "ENTER_YOUR_NANO_PRIVATE_KEY",
|
|
89
|
+
"NANO_MAX_SEND_AMOUNT": "ENTER_A_NEW_MAX_SEND_AMOUNT"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Notes:
|
|
97
|
+
|
|
98
|
+
- ENTER_FULL_FILE_PATH_TO_NODE_DOT_EXE_ON_YOUR_SYSTEM should point to the `node.exe` executable in your Node.js installation e.g. `C:\\Program Files\\nodejs\\node.exe`
|
|
99
|
+
- ENTER_FULL_FILE_PATH_TO_NANO_CURRENCY_JS_FILE_FROM_THIS_REPOSITORY should point to the `nano-currency.js` file in this repository e.g. `C:\\projects\\nano-currency-mcp-server\\nano-currency.js`
|
|
100
|
+
- If you are using Windows you need to use double-backslashes in your file paths e.g. `C:\\Program Files\\nodejs\\node.exe`
|
|
101
|
+
- ENTER_YOUR_NANO_RPC_URL and ENTER_YOUR_NANO_WORK_GENERATION_URL may often be the same value, in that case just omit the NANO_WORK_GENERATION_URL line entirely. An example ENTER_YOUR_NANO_RPC_URL may look something like `http://localhost:7076`
|
|
102
|
+
- ENTER_YOUR_NANO_PRIVATE_KEY - This is π¨*NOT A WALLET SEED*π¨ but rather the **private key** for a Nano address you control. This key is used when signing Nano transactions and to derive your Nano address from. Please test with a private key for an address containing a small amount of Nano.
|
|
103
|
+
- ENTER_A_NEW_MAX_SEND_AMOUNT is optional but you may use it if you want to override the default send maximum which is 0.01 nano (ΣΎ0.01). Enter a numeric value only (without "nano" or "ΣΎ"). It is recommended to not set this or set it lower than the default when testing, as it will prevent sending higher amounts of Nano than expected due to possible LLM hallucinations.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
### 5. Save your changes to `claude_desktop_config.json` and restart Claude Desktop
|
|
107
|
+
|
|
108
|
+
### 6. If you have configured everything correctly you will see the following icon when you start up Claude Desktop
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
### Click on the icon to get a description of the tools installed
|
|
113
|
+
|
|
114
|
+

|
|
115
|
+
|
|
116
|
+
### 7. Try out the tools by prompting Claude Desktop with nano-related prompts
|
|
117
|
+
|
|
118
|
+
https://github.com/user-attachments/assets/c877cc5a-0847-416c-b169-a988cac796f9
|
|
119
|
+
|
|
120
|
+
## π¨ Disclaimer π¨
|
|
121
|
+
As always when working with real world value, in this case Nano, **be careful** when using this software. The authors and contributors shall not be held liable for any use of this software's functionality, intentional or unintentional, that leads to an undesired lose of funds.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
MIT
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/nano-currency.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Nano Currency MCP Server
|
|
3
|
+
* Provides tools to send Nano and retrieve account / block info via Nano node RPC
|
|
4
|
+
* nano_send - Send a specified amount of Nano currency
|
|
5
|
+
* nano_account_info - Retrieve detailed information about a specific Nano account/address
|
|
6
|
+
* nano_my_account_info - Retrieve detailed information about your predefined Nano account/address
|
|
7
|
+
* block_info - Retrieve detailed information about a specific Nano block
|
|
8
|
+
*
|
|
9
|
+
* Required environment variables:
|
|
10
|
+
* - NANO_RPC_URL
|
|
11
|
+
*
|
|
12
|
+
* Optional environment variables:
|
|
13
|
+
* - NANO_PRIVATE_KEY (Required for nano_send, nano_my_account_info. This is the private key for an address, NOT the wallet seed)
|
|
14
|
+
* - NANO_WORK_GENERATION_URL (Optional for nano_send; defaults to NANO_RPC_URL if not set)
|
|
15
|
+
* - NANO_MAX_SEND_AMOUNT (In nano units. Defaults to 0.01, use this variable to override the default)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
19
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'
|
|
20
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
21
|
+
import z from 'zod'
|
|
22
|
+
import * as N from 'nanocurrency'
|
|
23
|
+
import BigNumber from "bignumber.js"
|
|
24
|
+
|
|
25
|
+
const SERVER_NAME = 'nano_currency'
|
|
26
|
+
const VERSION = '1.0.1'
|
|
27
|
+
|
|
28
|
+
const NANO_MAX_SEND_AMOUNT_DEFAULT = 0.01
|
|
29
|
+
|
|
30
|
+
const FETCH_COMMON = {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ONE_SECOND = 1000 // in milliseconds
|
|
38
|
+
const ONE_MINUTE = 60 * ONE_SECOND
|
|
39
|
+
|
|
40
|
+
const NANO_RPC_URL_KEY = 'NANO_RPC_URL'
|
|
41
|
+
const NANO_WORK_GENERATION_URL_KEY = 'NANO_WORK_GENERATION_URL'
|
|
42
|
+
|
|
43
|
+
// -----
|
|
44
|
+
|
|
45
|
+
const NANO_PRIVATE_KEY_SCHEMA = z.string({
|
|
46
|
+
required_error: `NANO_PRIVATE_KEY is required`,
|
|
47
|
+
})
|
|
48
|
+
.refine(val => N.checkKey(val), { message: `NANO_PRIVATE_KEY is not valid` })
|
|
49
|
+
|
|
50
|
+
// -----
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const envVarsToCheck = ['NANO_RPC_URL']
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < envVarsToCheck.length; i++) {
|
|
56
|
+
z.string({
|
|
57
|
+
required_error: `${envVarsToCheck[i]} is required`,
|
|
58
|
+
}).parse(process.env[envVarsToCheck[i]])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Error:', error.message || error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// -----
|
|
67
|
+
|
|
68
|
+
async function rpcCall(envUrl, action, payload, timeout = ONE_MINUTE) {
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
const timer = setTimeout(() => controller.abort(), timeout)
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(process.env[envUrl], {
|
|
73
|
+
...FETCH_COMMON,
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
body: JSON.stringify({ action, ...payload }),
|
|
76
|
+
})
|
|
77
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
|
|
78
|
+
const json = await res.json()
|
|
79
|
+
if (json.error) throw new Error(`RPC Error: ${json.error}`)
|
|
80
|
+
return json
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new Error(`[${envUrl}] ${error.message}`)
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timer)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// -----
|
|
89
|
+
|
|
90
|
+
function createTextResponse(text) {
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
metadata: { server: SERVER_NAME, version: VERSION }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// -----
|
|
103
|
+
|
|
104
|
+
function createErrorResponse(error) {
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
isError: true,
|
|
113
|
+
errorCode: error instanceof McpError ? error.code : ErrorCode.INTERNAL_ERROR
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// -----
|
|
118
|
+
|
|
119
|
+
function convertRawToNano(amount) {
|
|
120
|
+
return N.convert(String(amount), {from: 'raw', to: 'Nano'})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -----
|
|
124
|
+
|
|
125
|
+
function convertNanoToRaw(amount) {
|
|
126
|
+
return N.convert(String(amount), {from: 'Nano', to: 'raw'})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// -----
|
|
130
|
+
|
|
131
|
+
function getAddress () {
|
|
132
|
+
return N.deriveAddress(N.derivePublicKey(process.env.NANO_PRIVATE_KEY), { useNanoPrefix: true })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// -----
|
|
136
|
+
|
|
137
|
+
function friendlyAmount (balance) {
|
|
138
|
+
return `${convertRawToNano(balance)} in nano units or ${balance} in raw units`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -----
|
|
142
|
+
|
|
143
|
+
const server = new McpServer(
|
|
144
|
+
{
|
|
145
|
+
name: SERVER_NAME,
|
|
146
|
+
version: VERSION
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
// -----
|
|
151
|
+
|
|
152
|
+
async function getAccountInfo(address) {
|
|
153
|
+
return (
|
|
154
|
+
await rpcCall(
|
|
155
|
+
NANO_RPC_URL_KEY,
|
|
156
|
+
'account_info',
|
|
157
|
+
{
|
|
158
|
+
account: address,
|
|
159
|
+
representative: 'true'
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// -----
|
|
166
|
+
// nano_send
|
|
167
|
+
|
|
168
|
+
const nano_send_parameters = {
|
|
169
|
+
destination_address: z.string({
|
|
170
|
+
required_error: `Destination address is required`,
|
|
171
|
+
})
|
|
172
|
+
.refine(address_ => N.checkAddress(address_), { message: 'Destination address is not valid' })
|
|
173
|
+
.describe('Nano address to send the nano to'),
|
|
174
|
+
amount: z.string({
|
|
175
|
+
required_error: `Amount is required`,
|
|
176
|
+
})
|
|
177
|
+
.refine(amount_ => !isNaN(Number(amount_)) && Number(amount_) > 0, { message: 'Amount must be a positive number' })
|
|
178
|
+
.transform(amount_ => Number(amount_))
|
|
179
|
+
.refine(amount_ => amount_ <= (process.env.NANO_MAX_SEND_AMOUNT || NANO_MAX_SEND_AMOUNT_DEFAULT), { message: 'Maximum send amount exceeded' })
|
|
180
|
+
.describe(`Amount of Nano to send (max ${(process.env.NANO_MAX_SEND_AMOUNT || NANO_MAX_SEND_AMOUNT_DEFAULT)} by default)`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
server.tool(
|
|
184
|
+
'nano_send',
|
|
185
|
+
'Send a specified amount of Nano currency from a predefined account to a destination Nano address.',
|
|
186
|
+
nano_send_parameters,
|
|
187
|
+
async function (parameters) {
|
|
188
|
+
|
|
189
|
+
NANO_PRIVATE_KEY_SCHEMA.parse(process.env.NANO_PRIVATE_KEY)
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
let amountInRaw = convertNanoToRaw(parameters.amount)
|
|
193
|
+
let sourceAddress = getAddress()
|
|
194
|
+
let sourceAddressInfo = await getAccountInfo(sourceAddress)
|
|
195
|
+
let balanceAfterSend = BigNumber(sourceAddressInfo.balance).minus(amountInRaw).toFixed()
|
|
196
|
+
|
|
197
|
+
if (BigNumber(sourceAddressInfo.balance).lt(amountInRaw)) {
|
|
198
|
+
throw new Error("Insufficient balance to perform Nano send transaction");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!sourceAddressInfo.frontier) {
|
|
202
|
+
throw new Error("Source account has no frontier (unopened account)");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// -----
|
|
206
|
+
|
|
207
|
+
let work = (
|
|
208
|
+
await rpcCall(
|
|
209
|
+
(process.env.NANO_WORK_GENERATION_URL ? NANO_WORK_GENERATION_URL_KEY : NANO_RPC_URL_KEY),
|
|
210
|
+
'work_generate',
|
|
211
|
+
{ hash: sourceAddressInfo.frontier },
|
|
212
|
+
5 * ONE_MINUTE
|
|
213
|
+
)
|
|
214
|
+
).work
|
|
215
|
+
|
|
216
|
+
z.string({
|
|
217
|
+
required_error: `Work is required`,
|
|
218
|
+
}).refine(work_ => N.validateWork({ work: work_, blockHash: sourceAddressInfo.frontier }), { message: 'Computed Proof-of-Work for Nano transaction is not valid' }).parse(work)
|
|
219
|
+
|
|
220
|
+
// -----
|
|
221
|
+
|
|
222
|
+
let { block } = N.createBlock(process.env.NANO_PRIVATE_KEY, {
|
|
223
|
+
representative: sourceAddressInfo.representative,
|
|
224
|
+
balance: balanceAfterSend,
|
|
225
|
+
work,
|
|
226
|
+
link: parameters.destination_address,
|
|
227
|
+
previous: sourceAddressInfo.frontier
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
let processJson = (
|
|
231
|
+
await rpcCall(
|
|
232
|
+
NANO_RPC_URL_KEY,
|
|
233
|
+
'process',
|
|
234
|
+
{
|
|
235
|
+
json_block: 'true',
|
|
236
|
+
subtype: 'send',
|
|
237
|
+
block
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return createTextResponse(JSON.stringify(processJson))
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.error('[nano_send] Error:', error.message || error);
|
|
246
|
+
return createErrorResponse(error)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
// -----
|
|
252
|
+
// nano_account_info
|
|
253
|
+
|
|
254
|
+
const nano_account_info_parameters = {
|
|
255
|
+
address: z.string({ required_error: 'Address is required' })
|
|
256
|
+
.refine(address_ => N.checkAddress(address_), { message: 'Nano address is not valid' })
|
|
257
|
+
.describe("Nano address/account to get information about")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
server.tool(
|
|
261
|
+
'nano_account_info',
|
|
262
|
+
'Retrieve detailed information about a specific Nano account/address, including balance (in Nano and raw units), representative, and frontier block.',
|
|
263
|
+
nano_account_info_parameters,
|
|
264
|
+
async function (parameters) {
|
|
265
|
+
try {
|
|
266
|
+
let accountInfo = await getAccountInfo(parameters.address)
|
|
267
|
+
|
|
268
|
+
return createTextResponse(`The account information for ${parameters.address} is ` + JSON.stringify({ ...accountInfo, balance: friendlyAmount(accountInfo.balance) }))
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
console.error('[nano_account_info] Error:', error.message || error);
|
|
272
|
+
return createErrorResponse(error)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
// -----
|
|
278
|
+
// nano_my_account_info
|
|
279
|
+
|
|
280
|
+
server.tool(
|
|
281
|
+
'nano_my_account_info',
|
|
282
|
+
'Retrieve detailed information about my Nano account/address, including balance (in Nano and raw units), representative, and frontier block. This is the account that is used to send Nano from.',
|
|
283
|
+
{},
|
|
284
|
+
async function () {
|
|
285
|
+
try {
|
|
286
|
+
NANO_PRIVATE_KEY_SCHEMA.parse(process.env.NANO_PRIVATE_KEY)
|
|
287
|
+
|
|
288
|
+
const myAddress = getAddress()
|
|
289
|
+
let myAccountInfo = await getAccountInfo(myAddress)
|
|
290
|
+
|
|
291
|
+
return createTextResponse(`The account information for ${myAddress} is ` + JSON.stringify({ ...myAccountInfo, balance: friendlyAmount(myAccountInfo.balance) }))
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
console.error('[nano_my_account_info] Error:', error.message || error);
|
|
295
|
+
return createErrorResponse(error)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
// -----
|
|
301
|
+
// block_info
|
|
302
|
+
|
|
303
|
+
const block_info_parameters = {
|
|
304
|
+
hash: z.string({ required_error: 'Block hash is required' })
|
|
305
|
+
.refine(hash_ => N.checkHash(hash_), { message: 'Block hash is not valid' })
|
|
306
|
+
.describe("Hash for the Nano block to get information about")
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
server.tool(
|
|
310
|
+
'block_info',
|
|
311
|
+
'Retrieve detailed information about a specific Nano block.',
|
|
312
|
+
block_info_parameters,
|
|
313
|
+
async function (parameters) {
|
|
314
|
+
try {
|
|
315
|
+
|
|
316
|
+
let blockInfoJson = (
|
|
317
|
+
await rpcCall(
|
|
318
|
+
NANO_RPC_URL_KEY,
|
|
319
|
+
'block_info',
|
|
320
|
+
{
|
|
321
|
+
json_block: 'true',
|
|
322
|
+
hash: parameters.hash
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return createTextResponse(
|
|
328
|
+
`The block information for hash ${parameters.hash} is ` +
|
|
329
|
+
JSON.stringify({
|
|
330
|
+
...blockInfoJson,
|
|
331
|
+
amount: blockInfoJson.amount ? friendlyAmount(blockInfoJson.amount) : 'N/A',
|
|
332
|
+
balance: blockInfoJson.balance ? friendlyAmount(blockInfoJson.balance) : 'N/A'
|
|
333
|
+
})
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
console.error('[block_info] Error:', error.message || error);
|
|
338
|
+
return createErrorResponse(error)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
// -----
|
|
344
|
+
// nano_send
|
|
345
|
+
|
|
346
|
+
const x402_payment_parameters = {
|
|
347
|
+
x402_version: z.number({
|
|
348
|
+
required_error: `Destination address is required`,
|
|
349
|
+
})
|
|
350
|
+
.refine(x402Version_ => !isNaN(Number(x402Version_)) && Number(x402Version_) > 0, { message: 'x402 version must be a positive number' }),
|
|
351
|
+
pay_to: z.string({
|
|
352
|
+
required_error: `Destination address is required`,
|
|
353
|
+
})
|
|
354
|
+
.refine(address_ => N.checkAddress(address_), { message: 'Destination address is not valid' })
|
|
355
|
+
.describe('Nano address to send the nano to'),
|
|
356
|
+
pay_amount: z.string({
|
|
357
|
+
required_error: `Amount is required`,
|
|
358
|
+
})
|
|
359
|
+
.refine(amount_ => !isNaN(Number(amount_)) && Number(amount_) > 0, { message: 'Amount must be a positive number' })
|
|
360
|
+
.transform(amount_ => Number(amount_))
|
|
361
|
+
.refine(amount_ => amount_ <= (process.env.NANO_MAX_SEND_AMOUNT || NANO_MAX_SEND_AMOUNT_DEFAULT), { message: 'Maximum send amount exceeded' })
|
|
362
|
+
.describe(`Amount of Nano to send (max ${(process.env.NANO_MAX_SEND_AMOUNT || NANO_MAX_SEND_AMOUNT_DEFAULT)} by default)`),
|
|
363
|
+
payment_id: z.string()
|
|
364
|
+
.describe('Payment ID'),
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
server.tool(
|
|
368
|
+
'x402_payment',
|
|
369
|
+
'Create the contents for the X-PAYMENT x402 header',
|
|
370
|
+
x402_payment_parameters,
|
|
371
|
+
async function (parameters) {
|
|
372
|
+
|
|
373
|
+
NANO_PRIVATE_KEY_SCHEMA.parse(process.env.NANO_PRIVATE_KEY)
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
let amountInRaw = convertNanoToRaw(parameters.pay_amount)
|
|
377
|
+
let sourceAddress = getAddress()
|
|
378
|
+
let sourceAddressInfo = await getAccountInfo(sourceAddress)
|
|
379
|
+
let balanceAfterSend = BigNumber(sourceAddressInfo.balance).minus(amountInRaw).toFixed()
|
|
380
|
+
|
|
381
|
+
if (BigNumber(sourceAddressInfo.balance).lt(amountInRaw)) {
|
|
382
|
+
throw new Error("Insufficient balance to perform Nano send transaction");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!sourceAddressInfo.frontier) {
|
|
386
|
+
throw new Error("Source account has no frontier (unopened account)");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// -----
|
|
390
|
+
|
|
391
|
+
let work = (
|
|
392
|
+
await rpcCall(
|
|
393
|
+
(process.env.NANO_WORK_GENERATION_URL ? NANO_WORK_GENERATION_URL_KEY : NANO_RPC_URL_KEY),
|
|
394
|
+
'work_generate',
|
|
395
|
+
{ hash: sourceAddressInfo.frontier },
|
|
396
|
+
5 * ONE_MINUTE
|
|
397
|
+
)
|
|
398
|
+
).work
|
|
399
|
+
|
|
400
|
+
z.string({
|
|
401
|
+
required_error: `Work is required`,
|
|
402
|
+
}).refine(work_ => N.validateWork({ work: work_, blockHash: sourceAddressInfo.frontier }), { message: 'Computed Proof-of-Work for Nano transaction is not valid' }).parse(work)
|
|
403
|
+
|
|
404
|
+
// -----
|
|
405
|
+
|
|
406
|
+
let { block } = N.createBlock(process.env.NANO_PRIVATE_KEY, {
|
|
407
|
+
representative: sourceAddressInfo.representative,
|
|
408
|
+
balance: balanceAfterSend,
|
|
409
|
+
work,
|
|
410
|
+
link: parameters.pay_to,
|
|
411
|
+
previous: sourceAddressInfo.frontier
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
if (x402_version === 1) {
|
|
415
|
+
return createTextResponse(JSON.stringify(
|
|
416
|
+
{
|
|
417
|
+
x402Version: x402_version,
|
|
418
|
+
payload: {
|
|
419
|
+
block,
|
|
420
|
+
paymentId: payment_id
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
))
|
|
424
|
+
} else {
|
|
425
|
+
throw new Error("x402 version not supported");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
console.error('[x402_payment] Error:', error.message || error);
|
|
430
|
+
return createErrorResponse(error)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
// -----
|
|
436
|
+
|
|
437
|
+
async function main() {
|
|
438
|
+
const transport = new StdioServerTransport()
|
|
439
|
+
await server.connect(transport)
|
|
440
|
+
console.error(`${SERVER_NAME} MCP Server running on stdio`)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
main().catch((error) => {
|
|
444
|
+
console.error(`[startup] ${SERVER_NAME} MCP Server Error:`, error.message || error)
|
|
445
|
+
process.exit(1)
|
|
446
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nano-currency-mcp-server",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Send Nano currency from AI agents/LLMs",
|
|
5
|
+
"main": "nano-currency.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+ssh://git@github.com/kilkelly/nano-currency-mcp-server.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"nano",
|
|
13
|
+
"nanocurrency",
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"ai",
|
|
17
|
+
"ai-agent",
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"ai-assistant",
|
|
20
|
+
"ai-assistants",
|
|
21
|
+
"agent",
|
|
22
|
+
"agents",
|
|
23
|
+
"assistant",
|
|
24
|
+
"assistants",
|
|
25
|
+
"llm",
|
|
26
|
+
"llms",
|
|
27
|
+
"crypto"
|
|
28
|
+
],
|
|
29
|
+
"author": "Frank Kilkelly",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/kilkelly/nano-currency-mcp-server/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/kilkelly/nano-currency-mcp-server#readme",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "1.7.0",
|
|
37
|
+
"bignumber.js": "9.1.2",
|
|
38
|
+
"nanocurrency": "2.5.0",
|
|
39
|
+
"zod": "3.24.2"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
|
|
2
|
+
|
|
3
|
+
startCommand:
|
|
4
|
+
type: stdio
|
|
5
|
+
configSchema:
|
|
6
|
+
# JSON Schema defining the configuration options for the MCP.
|
|
7
|
+
type: object
|
|
8
|
+
required:
|
|
9
|
+
- nanoRpcUrl
|
|
10
|
+
properties:
|
|
11
|
+
nanoRpcUrl:
|
|
12
|
+
type: string
|
|
13
|
+
description: URL to communicate with a Nano node RPC (required)
|
|
14
|
+
nanoWorkGenerationUrl:
|
|
15
|
+
type: string
|
|
16
|
+
description: URL to communicate with a Nano work generation RPC (optional)
|
|
17
|
+
nanoPrivateKey:
|
|
18
|
+
type: string
|
|
19
|
+
description: Nano private key for signing transactions (optional for info tools,
|
|
20
|
+
required for send)
|
|
21
|
+
nanoMaxSendAmount:
|
|
22
|
+
type: number
|
|
23
|
+
description: Maximum amount of Nano allowed per transaction (optional)
|
|
24
|
+
commandFunction:
|
|
25
|
+
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|
|
26
|
+
|-
|
|
27
|
+
(config) => ({
|
|
28
|
+
command: 'node',
|
|
29
|
+
args: ['nano-currency.js'],
|
|
30
|
+
env: {
|
|
31
|
+
NANO_RPC_URL: config.nanoRpcUrl,
|
|
32
|
+
...(config.nanoWorkGenerationUrl ? { NANO_WORK_GENERATION_URL: config.nanoWorkGenerationUrl } : {}),
|
|
33
|
+
...(config.nanoPrivateKey ? { NANO_PRIVATE_KEY: config.nanoPrivateKey } : {}),
|
|
34
|
+
...(config.nanoMaxSendAmount !== undefined ? { NANO_MAX_SEND_AMOUNT: String(config.nanoMaxSendAmount) } : {})
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
exampleConfig:
|
|
38
|
+
nanoRpcUrl: http://localhost:7076
|
|
39
|
+
nanoWorkGenerationUrl: http://localhost:7076
|
|
40
|
+
nanoPrivateKey: E3F2A1D4B7C89E6F1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4
|
|
41
|
+
nanoMaxSendAmount: 0.05
|