nyxora 1.6.4 → 1.6.6
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/README.md +13 -3
- package/launcher.ts +9 -0
- package/package.json +4 -1
- package/packages/core/package.json +2 -2
- package/packages/core/src/gateway/cli.ts +3 -0
- package/packages/core/src/utils/safeLogger.js +59 -0
- package/packages/core/src/utils/safeLogger.ts +60 -0
- package/packages/dashboard/package.json +1 -1
- package/packages/mcp-server/dist/server.js +111 -0
- package/packages/mcp-server/package.json +19 -0
- package/packages/mcp-server/src/server.ts +124 -0
- package/packages/mcp-server/tsconfig.json +15 -0
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
- package/packages/policy/package.json +2 -2
- package/packages/policy/src/server.ts +3 -0
- package/packages/signer/package.json +2 -2
- package/packages/signer/src/server.ts +15 -0
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# Nyxora Agent 🤖
|
|
2
2
|
**Production-Grade Secure AI Execution Framework for Web3 Agents.**
|
|
3
3
|
|
|
4
|
-
[](https://github.com/perasyudha/Nyxora)
|
|
5
|
+
[](#)
|
|
5
6
|
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
[](#️-advanced-security-threat-model)
|
|
7
8
|
[](#️-advanced-security-threat-model)
|
|
8
9
|
[](#️-advanced-security-threat-model)
|
|
9
10
|
|
|
10
|
-
Nyxora (v1.6.
|
|
11
|
+
Nyxora (v1.6.5) is a **secure, non-custodial runtime infrastructure for autonomous onchain agents** built with a robust Monorepo architecture (Node.js & React). Designed for autonomous workflows with a premium Glassmorphism UI dashboard and strict client-side key isolation.
|
|
12
|
+
|
|
13
|
+
**Nyxora now natively supports the Model Context Protocol (MCP)**. You can transform your external AI agents (like Claude Desktop and Cursor) into secure Web3 actors that execute swaps and fetch balances using Nyxora's secure signer vault. [View the MCP Integration Guide](https://perasyudha.github.io/Nyxora/guide/mcp-integration)
|
|
11
14
|
|
|
12
15
|
It operates under an institutional-grade **Cryptographically Bound Human-in-the-Loop** execution model, ensuring that Remote AIs (LLMs) never have unilateral access to your funds.
|
|
13
16
|
|
|
@@ -15,7 +18,7 @@ It operates under an institutional-grade **Cryptographically Bound Human-in-the-
|
|
|
15
18
|
|
|
16
19
|
## 🔥 Key Features
|
|
17
20
|
|
|
18
|
-
### Advanced Security Architecture
|
|
21
|
+
### Advanced Security Architecture
|
|
19
22
|
* **3-Tier IPC Architecture**: Nyxora is split into isolated processes: **Core** (LLM Runtime), **Policy Engine** (Guardrails on port 3001), and **Signer Vault** (Isolated Key Manager on Unix Sockets).
|
|
20
23
|
* **Cryptographically Bound Approval**: Policy changes and transactions requested by the AI are drafted as hashes (`sha256`). Approval via the UI requires a challenge nonce, preventing Man-in-the-Middle (MITM) attacks.
|
|
21
24
|
* **Immutable Policy Guardrails**: Transaction limits (e.g. `max_usd_per_tx`) are strictly enforced by the Policy Engine. The LLM has zero write-access to bypass these rules.
|
|
@@ -111,5 +114,12 @@ For complete technical deep-dives into our Cryptographic Architecture, please vi
|
|
|
111
114
|
|
|
112
115
|
*(Includes guides on Secure Wallet Imports, Architecture Blueprints, Troubleshooting, and Custom Skill Development).*
|
|
113
116
|
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
**❤️ Support the Project**
|
|
120
|
+
|
|
121
|
+
Building and maintaining a highly secure, zero-trust architecture takes significant time and resources. If you love what we are building, you can help us keep Nyxora open, secure, and constantly evolving by sending a coffee our way:
|
|
122
|
+
- **EVM:** `0x18a30d5db50d287dba669c5672cd71246cc4c4c6`
|
|
123
|
+
|
|
114
124
|
---
|
|
115
125
|
**License:** MIT License
|
package/launcher.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { initSafeLogger } from './packages/core/src/utils/safeLogger';
|
|
2
|
+
initSafeLogger();
|
|
3
|
+
|
|
1
4
|
import { spawn } from 'child_process';
|
|
2
5
|
import crypto from 'crypto';
|
|
3
6
|
import fs from 'fs';
|
|
@@ -6,6 +9,12 @@ import path from 'path';
|
|
|
6
9
|
const INTERNAL_AUTH_TOKEN = crypto.randomBytes(64).toString('hex');
|
|
7
10
|
console.log(`[Launcher] Generated Internal Auth Token: ${INTERNAL_AUTH_TOKEN.substring(0, 8)}...`);
|
|
8
11
|
|
|
12
|
+
const nyxoraDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.nyxora');
|
|
13
|
+
if (!fs.existsSync(nyxoraDir)) fs.mkdirSync(nyxoraDir, { recursive: true, mode: 0o700 });
|
|
14
|
+
const tokenPath = path.join(nyxoraDir, 'runtime.token');
|
|
15
|
+
fs.writeFileSync(tokenPath, INTERNAL_AUTH_TOKEN, { mode: 0o600 });
|
|
16
|
+
console.log(`[Launcher] Secured runtime token at ${tokenPath} (0600)`);
|
|
17
|
+
|
|
9
18
|
const env = {
|
|
10
19
|
...process.env,
|
|
11
20
|
INTERNAL_AUTH_TOKEN,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nyxora",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"packages/*"
|
|
@@ -19,17 +19,20 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@clack/prompts": "^1.4.0",
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.5.0",
|
|
22
23
|
"concurrently": "^9.2.1",
|
|
23
24
|
"cors": "^2.8.6",
|
|
24
25
|
"dotenv": "^17.4.2",
|
|
25
26
|
"express": "^5.2.1",
|
|
26
27
|
"express-rate-limit": "^7.5.0",
|
|
27
28
|
"helmet": "^8.0.0",
|
|
29
|
+
"isolated-vm": "^6.1.2",
|
|
28
30
|
"jsonwebtoken": "^9.0.2",
|
|
29
31
|
"@napi-rs/keyring": "^1.3.0",
|
|
30
32
|
"open": "^11.0.0",
|
|
31
33
|
"openai": "^6.39.0",
|
|
32
34
|
"picocolors": "^1.1.1",
|
|
35
|
+
"telegraf": "^4.16.3",
|
|
33
36
|
"ts-node": "^10.9.2",
|
|
34
37
|
"typescript": "^6.0.3",
|
|
35
38
|
"viem": "^2.51.0",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initSafeLogger = initSafeLogger;
|
|
4
|
+
exports.safeLog = safeLog;
|
|
5
|
+
exports.safeError = safeError;
|
|
6
|
+
exports.safeWarn = safeWarn;
|
|
7
|
+
const PRIVATE_KEY_REGEX = /0x[a-fA-F0-9]{64}/g;
|
|
8
|
+
const JWT_REGEX = /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g;
|
|
9
|
+
function sanitize(args) {
|
|
10
|
+
return args.map(arg => {
|
|
11
|
+
if (typeof arg === 'string') {
|
|
12
|
+
return arg
|
|
13
|
+
.replace(PRIVATE_KEY_REGEX, '[REDACTED_PRIVATE_KEY]')
|
|
14
|
+
.replace(JWT_REGEX, '[REDACTED_JWT]');
|
|
15
|
+
}
|
|
16
|
+
if (arg instanceof Error) {
|
|
17
|
+
const sanitizedError = new Error(arg.message
|
|
18
|
+
.replace(PRIVATE_KEY_REGEX, '[REDACTED_PRIVATE_KEY]')
|
|
19
|
+
.replace(JWT_REGEX, '[REDACTED_JWT]'));
|
|
20
|
+
sanitizedError.stack = arg.stack?.replace(PRIVATE_KEY_REGEX, '[REDACTED_PRIVATE_KEY]').replace(JWT_REGEX, '[REDACTED_JWT]');
|
|
21
|
+
return sanitizedError;
|
|
22
|
+
}
|
|
23
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
24
|
+
try {
|
|
25
|
+
const str = JSON.stringify(arg);
|
|
26
|
+
const sanitizedStr = str
|
|
27
|
+
.replace(PRIVATE_KEY_REGEX, '[REDACTED_PRIVATE_KEY]')
|
|
28
|
+
.replace(JWT_REGEX, '[REDACTED_JWT]');
|
|
29
|
+
return JSON.parse(sanitizedStr);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return arg;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return arg;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const originalLog = console.log.bind(console);
|
|
39
|
+
const originalError = console.error.bind(console);
|
|
40
|
+
const originalWarn = console.warn.bind(console);
|
|
41
|
+
function initSafeLogger() {
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
if (console.__isSafe)
|
|
44
|
+
return;
|
|
45
|
+
console.log = (...args) => originalLog(...sanitize(args));
|
|
46
|
+
console.error = (...args) => originalError(...sanitize(args));
|
|
47
|
+
console.warn = (...args) => originalWarn(...sanitize(args));
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
console.__isSafe = true;
|
|
50
|
+
}
|
|
51
|
+
function safeLog(...args) {
|
|
52
|
+
originalLog(...sanitize(args));
|
|
53
|
+
}
|
|
54
|
+
function safeError(...args) {
|
|
55
|
+
originalError(...sanitize(args));
|
|
56
|
+
}
|
|
57
|
+
function safeWarn(...args) {
|
|
58
|
+
originalWarn(...sanitize(args));
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const PRIVATE_KEY_REGEX = /0x[a-fA-F0-9]{64}/g;
|
|
2
|
+
const JWT_REGEX = /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g;
|
|
3
|
+
|
|
4
|
+
function sanitize(args: any[]): any[] {
|
|
5
|
+
return args.map(arg => {
|
|
6
|
+
if (typeof arg === 'string') {
|
|
7
|
+
return arg
|
|
8
|
+
.replace(PRIVATE_KEY_REGEX, '[REDACTED_PRIVATE_KEY]')
|
|
9
|
+
.replace(JWT_REGEX, '[REDACTED_JWT]');
|
|
10
|
+
}
|
|
11
|
+
if (arg instanceof Error) {
|
|
12
|
+
const sanitizedError = new Error(arg.message
|
|
13
|
+
.replace(PRIVATE_KEY_REGEX, '[REDACTED_PRIVATE_KEY]')
|
|
14
|
+
.replace(JWT_REGEX, '[REDACTED_JWT]')
|
|
15
|
+
);
|
|
16
|
+
sanitizedError.stack = arg.stack?.replace(PRIVATE_KEY_REGEX, '[REDACTED_PRIVATE_KEY]').replace(JWT_REGEX, '[REDACTED_JWT]');
|
|
17
|
+
return sanitizedError;
|
|
18
|
+
}
|
|
19
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
20
|
+
try {
|
|
21
|
+
const str = JSON.stringify(arg);
|
|
22
|
+
const sanitizedStr = str
|
|
23
|
+
.replace(PRIVATE_KEY_REGEX, '[REDACTED_PRIVATE_KEY]')
|
|
24
|
+
.replace(JWT_REGEX, '[REDACTED_JWT]');
|
|
25
|
+
return JSON.parse(sanitizedStr);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return arg;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return arg;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const originalLog = console.log.bind(console);
|
|
35
|
+
const originalError = console.error.bind(console);
|
|
36
|
+
const originalWarn = console.warn.bind(console);
|
|
37
|
+
|
|
38
|
+
export function initSafeLogger() {
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
if (console.__isSafe) return;
|
|
41
|
+
|
|
42
|
+
console.log = (...args: any[]) => originalLog(...sanitize(args));
|
|
43
|
+
console.error = (...args: any[]) => originalError(...sanitize(args));
|
|
44
|
+
console.warn = (...args: any[]) => originalWarn(...sanitize(args));
|
|
45
|
+
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
console.__isSafe = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function safeLog(...args: any[]) {
|
|
51
|
+
originalLog(...sanitize(args));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function safeError(...args: any[]) {
|
|
55
|
+
originalError(...sanitize(args));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function safeWarn(...args: any[]) {
|
|
59
|
+
originalWarn(...sanitize(args));
|
|
60
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// MCP Server uses Stdio, so we avoid global hooks that might write to stdout.
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const http_1 = __importDefault(require("http"));
|
|
11
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
let JWT_SECRET = process.env.INTERNAL_AUTH_TOKEN;
|
|
15
|
+
if (!JWT_SECRET) {
|
|
16
|
+
try {
|
|
17
|
+
const tokenPath = path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', '.nyxora', 'runtime.token');
|
|
18
|
+
JWT_SECRET = fs_1.default.readFileSync(tokenPath, 'utf8').trim();
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
console.error("Missing INTERNAL_AUTH_TOKEN in mcp-server process and could not read ~/.nyxora/runtime.token");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const server = new mcp_js_1.McpServer({
|
|
26
|
+
name: "Nyxora MCP Server",
|
|
27
|
+
version: "1.6.5"
|
|
28
|
+
});
|
|
29
|
+
// Helper to make internal requests to Policy Engine
|
|
30
|
+
function callPolicyEngine(path, method, payload) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const dataString = payload ? JSON.stringify(payload) : '';
|
|
33
|
+
const options = {
|
|
34
|
+
hostname: '127.0.0.1',
|
|
35
|
+
port: 3001,
|
|
36
|
+
path: path,
|
|
37
|
+
method: method,
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'Authorization': `Bearer ${jsonwebtoken_1.default.sign({ service: 'mcp-server' }, JWT_SECRET, { expiresIn: '1m' })}`,
|
|
41
|
+
'Content-Length': Buffer.byteLength(dataString)
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const req = http_1.default.request(options, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
res.on('data', chunk => data += chunk);
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(data);
|
|
50
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
51
|
+
reject(new Error(`Policy Engine Rejected (${res.statusCode}): ${parsed.error || parsed.message || data}`));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
resolve(parsed);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
reject(new Error(`Failed to parse Policy Engine response: ${data}`));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
req.on('error', (e) => reject(new Error(`Policy Engine Connection Error: ${e.message}`)));
|
|
63
|
+
if (dataString) {
|
|
64
|
+
req.write(dataString);
|
|
65
|
+
}
|
|
66
|
+
req.end();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// Tool: get_wallet_address
|
|
70
|
+
server.tool("get_wallet_address", "Fetch the secure Ethereum wallet address managed by the Nyxora Signer Vault. Use this to know the agent's identity.", {}, async () => {
|
|
71
|
+
try {
|
|
72
|
+
const response = await callPolicyEngine('/address', 'GET');
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: `Wallet Address: ${response.address}` }]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return {
|
|
79
|
+
isError: true,
|
|
80
|
+
content: [{ type: "text", text: e.message }]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// Tool: request_transaction
|
|
85
|
+
server.tool("request_transaction", "Request an EVM transaction (transfer, swap, bridge). This will be evaluated by the Policy Engine. If approved, it returns a pending Transaction ID.", {
|
|
86
|
+
type: zod_1.z.enum(['transfer', 'swap', 'bridge', 'mint', 'custom']).describe("The type of transaction"),
|
|
87
|
+
chainName: zod_1.z.string().describe("The target blockchain network (e.g. 'ethereum', 'base', 'arbitrum')"),
|
|
88
|
+
details: zod_1.z.any().describe("Detailed payload. For 'transfer': { to: string, amountWei: string }. For 'swap': { tokenIn: string, tokenOut: string, amountInWei: string }.")
|
|
89
|
+
}, async (args) => {
|
|
90
|
+
try {
|
|
91
|
+
const response = await callPolicyEngine('/request-tx', 'POST', args);
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: `Transaction requested successfully. Transaction ID: ${response.id}. Status: ${response.status}` }]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return {
|
|
98
|
+
isError: true,
|
|
99
|
+
content: [{ type: "text", text: `Transaction Request Failed! ${e.message}` }]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
async function main() {
|
|
104
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
105
|
+
await server.connect(transport);
|
|
106
|
+
console.error("Nyxora MCP Server running on stdio"); // stdio logs use stderr so stdout is purely for MCP JSON-RPC
|
|
107
|
+
}
|
|
108
|
+
main().catch((error) => {
|
|
109
|
+
console.error("Fatal error in MCP Server:", error);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nyxora-mcp-server",
|
|
3
|
+
"version": "1.6.6",
|
|
4
|
+
"description": "Nyxora MCP Server for external AI clients",
|
|
5
|
+
"main": "dist/server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"start": "node dist/server.js",
|
|
9
|
+
"dev": "ts-node src/server.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.5.0",
|
|
13
|
+
"zod": "^3.24.2"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.0.0",
|
|
17
|
+
"@types/node": "^20.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// MCP Server uses Stdio, so we avoid global hooks that might write to stdout.
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import jwt from 'jsonwebtoken';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
let JWT_SECRET = process.env.INTERNAL_AUTH_TOKEN;
|
|
12
|
+
|
|
13
|
+
if (!JWT_SECRET) {
|
|
14
|
+
try {
|
|
15
|
+
const tokenPath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.nyxora', 'runtime.token');
|
|
16
|
+
JWT_SECRET = fs.readFileSync(tokenPath, 'utf8').trim();
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error("Missing INTERNAL_AUTH_TOKEN in mcp-server process and could not read ~/.nyxora/runtime.token");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: "Nyxora MCP Server",
|
|
25
|
+
version: "1.6.5"
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Helper to make internal requests to Policy Engine
|
|
29
|
+
function callPolicyEngine(path: string, method: string, payload?: any): Promise<any> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const dataString = payload ? JSON.stringify(payload) : '';
|
|
32
|
+
|
|
33
|
+
const options = {
|
|
34
|
+
hostname: '127.0.0.1',
|
|
35
|
+
port: 3001,
|
|
36
|
+
path: path,
|
|
37
|
+
method: method,
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'Authorization': `Bearer ${jwt.sign({ service: 'mcp-server' }, JWT_SECRET as string, { expiresIn: '1m' })}`,
|
|
41
|
+
'Content-Length': Buffer.byteLength(dataString)
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const req = http.request(options, (res) => {
|
|
46
|
+
let data = '';
|
|
47
|
+
res.on('data', chunk => data += chunk);
|
|
48
|
+
res.on('end', () => {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(data);
|
|
51
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
52
|
+
reject(new Error(`Policy Engine Rejected (${res.statusCode}): ${parsed.error || parsed.message || data}`));
|
|
53
|
+
} else {
|
|
54
|
+
resolve(parsed);
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
reject(new Error(`Failed to parse Policy Engine response: ${data}`));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
req.on('error', (e) => reject(new Error(`Policy Engine Connection Error: ${e.message}`)));
|
|
63
|
+
|
|
64
|
+
if (dataString) {
|
|
65
|
+
req.write(dataString);
|
|
66
|
+
}
|
|
67
|
+
req.end();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Tool: get_wallet_address
|
|
72
|
+
server.tool(
|
|
73
|
+
"get_wallet_address",
|
|
74
|
+
"Fetch the secure Ethereum wallet address managed by the Nyxora Signer Vault. Use this to know the agent's identity.",
|
|
75
|
+
{},
|
|
76
|
+
async () => {
|
|
77
|
+
try {
|
|
78
|
+
const response = await callPolicyEngine('/address', 'GET');
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: `Wallet Address: ${response.address}` }]
|
|
81
|
+
};
|
|
82
|
+
} catch (e: any) {
|
|
83
|
+
return {
|
|
84
|
+
isError: true,
|
|
85
|
+
content: [{ type: "text", text: e.message }]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Tool: request_transaction
|
|
92
|
+
server.tool(
|
|
93
|
+
"request_transaction",
|
|
94
|
+
"Request an EVM transaction (transfer, swap, bridge). This will be evaluated by the Policy Engine. If approved, it returns a pending Transaction ID.",
|
|
95
|
+
{
|
|
96
|
+
type: z.enum(['transfer', 'swap', 'bridge', 'mint', 'custom']).describe("The type of transaction"),
|
|
97
|
+
chainName: z.string().describe("The target blockchain network (e.g. 'ethereum', 'base', 'arbitrum')"),
|
|
98
|
+
details: z.any().describe("Detailed payload. For 'transfer': { to: string, amountWei: string }. For 'swap': { tokenIn: string, tokenOut: string, amountInWei: string }.")
|
|
99
|
+
},
|
|
100
|
+
async (args) => {
|
|
101
|
+
try {
|
|
102
|
+
const response = await callPolicyEngine('/request-tx', 'POST', args);
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: `Transaction requested successfully. Transaction ID: ${response.id}. Status: ${response.status}` }]
|
|
105
|
+
};
|
|
106
|
+
} catch (e: any) {
|
|
107
|
+
return {
|
|
108
|
+
isError: true,
|
|
109
|
+
content: [{ type: "text", text: `Transaction Request Failed! ${e.message}` }]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const transport = new StdioServerTransport();
|
|
117
|
+
await server.connect(transport);
|
|
118
|
+
console.error("Nyxora MCP Server running on stdio"); // stdio logs use stderr so stdout is purely for MCP JSON-RPC
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch((error) => {
|
|
122
|
+
console.error("Fatal error in MCP Server:", error);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/server.ts"],"version":"6.0.3"}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { initSafeLogger } from '../../core/src/utils/safeLogger';
|
|
2
|
+
initSafeLogger();
|
|
3
|
+
|
|
1
4
|
import express from 'express';
|
|
2
5
|
import fs from 'fs';
|
|
3
6
|
import jwt from 'jsonwebtoken';
|
|
@@ -43,6 +46,18 @@ async function loadPrivateKey() {
|
|
|
43
46
|
// Fallback to vault.key
|
|
44
47
|
const vaultPath = path.join(os.homedir(), '.nyxora', 'vault.key');
|
|
45
48
|
if (fs.existsSync(vaultPath)) {
|
|
49
|
+
const stats = fs.statSync(vaultPath);
|
|
50
|
+
const mode = stats.mode & 0o777;
|
|
51
|
+
if (os.platform() !== 'win32' && mode !== 0o600) {
|
|
52
|
+
console.error(`\n======================================================`);
|
|
53
|
+
console.error(`FATAL: Insecure permissions detected on vault.key`);
|
|
54
|
+
console.error(`File permissions must be strictly 0600 (-rw-------)`);
|
|
55
|
+
console.error(`Current permissions: 0${mode.toString(8)}`);
|
|
56
|
+
console.error(`Refusing to start. Please run: chmod 600 ${vaultPath}`);
|
|
57
|
+
console.error(`======================================================\n`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
const content = fs.readFileSync(vaultPath, 'utf8');
|
|
47
62
|
const match = content.match(/PRIVATE_KEY=(.+)/);
|
|
48
63
|
if (match && match[1]) {
|