@nullpay/mcp 1.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.
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NullPayBackendClient = void 0;
4
+ function mapBackendError(path, text) {
5
+ if (path === '/mcp/relay/create-invoice') {
6
+ if (text.includes('NULLPAY_MCP_SHARED_SECRET is not configured')) {
7
+ return 'Invoice creation is blocked because NULLPAY_MCP_SHARED_SECRET is missing on the backend. Set the same NULLPAY_MCP_SHARED_SECRET value in both the backend env and the MCP server env, then restart both processes.';
8
+ }
9
+ if (text.includes('Invalid MCP shared secret')) {
10
+ return 'Invoice creation is blocked because the MCP shared secret does not match. Set the same NULLPAY_MCP_SHARED_SECRET value in both the backend env and the MCP server env, then restart both processes.';
11
+ }
12
+ }
13
+ return text;
14
+ }
15
+ class NullPayBackendClient {
16
+ constructor(baseUrl, mcpSecret) {
17
+ this.baseUrl = baseUrl;
18
+ this.mcpSecret = mcpSecret;
19
+ }
20
+ buildUrl(path) {
21
+ return `${this.baseUrl.replace(/\/+$/, '')}${path}`;
22
+ }
23
+ async request(path, init) {
24
+ const response = await fetch(this.buildUrl(path), init);
25
+ if (!response.ok) {
26
+ const text = await response.text();
27
+ throw new Error(mapBackendError(path, text || `Backend request failed: ${response.status}`));
28
+ }
29
+ return await response.json();
30
+ }
31
+ async getUserProfile(addressHash) {
32
+ const response = await fetch(this.buildUrl(`/users/profile/${addressHash}`));
33
+ if (response.status === 404) {
34
+ return null;
35
+ }
36
+ if (!response.ok) {
37
+ throw new Error(`Failed to fetch user profile: ${response.status}`);
38
+ }
39
+ return await response.json();
40
+ }
41
+ async upsertUserProfile(body) {
42
+ return await this.request('/users/profile', {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify(body),
46
+ });
47
+ }
48
+ async createInvoiceRow(body) {
49
+ return await this.request('/invoices', {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify(body),
53
+ });
54
+ }
55
+ async updateInvoice(hash, body) {
56
+ return await this.request(`/invoices/${hash}`, {
57
+ method: 'PATCH',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify(body),
60
+ });
61
+ }
62
+ async updateCheckoutSession(id, body) {
63
+ return await this.request(`/checkout/sessions/${id}`, {
64
+ method: 'PATCH',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify(body),
67
+ });
68
+ }
69
+ async getInvoice(hash) {
70
+ return await this.request(`/invoice/${hash}`);
71
+ }
72
+ async getMerchantInvoices(merchantHash) {
73
+ return await this.request(`/invoices/merchant/${merchantHash}`);
74
+ }
75
+ async relayCreateInvoice(body) {
76
+ return await this.request('/mcp/relay/create-invoice', {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ ...(this.mcpSecret ? { 'x-nullpay-mcp-secret': this.mcpSecret } : {})
81
+ },
82
+ body: JSON.stringify(body),
83
+ });
84
+ }
85
+ async sponsorExecution(body) {
86
+ return await this.request('/dps/sponsor-sweep', {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify(body),
90
+ });
91
+ }
92
+ }
93
+ exports.NullPayBackendClient = NullPayBackendClient;
@@ -0,0 +1,3 @@
1
+ export declare function hashAddress(address: string): string;
2
+ export declare function encryptWithPassword(text: string, password: string): Promise<string>;
3
+ export declare function decryptWithPassword(payload: string, password: string): Promise<string>;
package/dist/crypto.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hashAddress = hashAddress;
7
+ exports.encryptWithPassword = encryptWithPassword;
8
+ exports.decryptWithPassword = decryptWithPassword;
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const ITERATIONS = 100000;
11
+ const KEY_LENGTH = 32;
12
+ const DIGEST = 'sha256';
13
+ function toBase64(buffer) {
14
+ return Buffer.from(buffer).toString('base64');
15
+ }
16
+ function fromBase64(value) {
17
+ return Buffer.from(value, 'base64');
18
+ }
19
+ function hashAddress(address) {
20
+ return crypto_1.default.createHash('sha256').update(address).digest('hex');
21
+ }
22
+ async function encryptWithPassword(text, password) {
23
+ const salt = crypto_1.default.randomBytes(16);
24
+ const iv = crypto_1.default.randomBytes(12);
25
+ const key = await new Promise((resolve, reject) => {
26
+ crypto_1.default.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (error, derivedKey) => {
27
+ if (error)
28
+ reject(error);
29
+ else
30
+ resolve(derivedKey);
31
+ });
32
+ });
33
+ const cipher = crypto_1.default.createCipheriv('aes-256-gcm', key, iv);
34
+ const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
35
+ const tag = cipher.getAuthTag();
36
+ return `${toBase64(salt)}:${toBase64(iv)}:${toBase64(Buffer.concat([ciphertext, tag]))}`;
37
+ }
38
+ async function decryptWithPassword(payload, password) {
39
+ const parts = payload.split(':');
40
+ if (parts.length !== 3) {
41
+ throw new Error('Invalid encrypted payload format');
42
+ }
43
+ const [saltPart, ivPart, cipherPart] = parts;
44
+ const salt = fromBase64(saltPart);
45
+ const iv = fromBase64(ivPart);
46
+ const cipherWithTag = fromBase64(cipherPart);
47
+ const ciphertext = cipherWithTag.subarray(0, cipherWithTag.length - 16);
48
+ const tag = cipherWithTag.subarray(cipherWithTag.length - 16);
49
+ const key = await new Promise((resolve, reject) => {
50
+ crypto_1.default.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (error, derivedKey) => {
51
+ if (error)
52
+ reject(error);
53
+ else
54
+ resolve(derivedKey);
55
+ });
56
+ });
57
+ try {
58
+ const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', key, iv);
59
+ decipher.setAuthTag(tag);
60
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
61
+ return plaintext.toString('utf8');
62
+ }
63
+ catch {
64
+ throw new Error('Incorrect password or corrupted data');
65
+ }
66
+ }
package/dist/esm.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function dynamicImport<T = any>(specifier: string): Promise<T>;
package/dist/esm.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dynamicImport = dynamicImport;
4
+ async function dynamicImport(specifier) {
5
+ const importer = new Function('s', 'return import(s);');
6
+ return await importer(specifier);
7
+ }
@@ -0,0 +1,19 @@
1
+ type JsonRpcId = string | number | null;
2
+ interface JsonRpcRequest {
3
+ jsonrpc: '2.0';
4
+ id?: JsonRpcId;
5
+ method: string;
6
+ params?: Record<string, any>;
7
+ }
8
+ export declare class StdioJsonRpcServer {
9
+ private readonly onRequest;
10
+ private buffer;
11
+ constructor(onRequest: (request: JsonRpcRequest) => Promise<unknown>);
12
+ start(): void;
13
+ private processBuffer;
14
+ private tryReadFramedMessage;
15
+ private tryReadPlainJsonMessage;
16
+ private handleMessage;
17
+ send(payload: unknown): void;
18
+ }
19
+ export {};
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StdioJsonRpcServer = void 0;
4
+ class StdioJsonRpcServer {
5
+ constructor(onRequest) {
6
+ this.onRequest = onRequest;
7
+ this.buffer = '';
8
+ }
9
+ start() {
10
+ process.stdin.setEncoding('utf8');
11
+ process.stdin.on('data', (chunk) => {
12
+ this.buffer += chunk;
13
+ this.processBuffer().catch((error) => {
14
+ console.error('[nullpay-mcp] transport error', error instanceof Error ? error.message : String(error));
15
+ this.send({
16
+ jsonrpc: '2.0',
17
+ error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
18
+ id: null,
19
+ });
20
+ });
21
+ });
22
+ }
23
+ async processBuffer() {
24
+ while (true) {
25
+ const framed = this.tryReadFramedMessage();
26
+ if (framed !== null) {
27
+ const request = JSON.parse(framed);
28
+ console.error('[nullpay-mcp] parsed framed request', request.method, request.id);
29
+ await this.handleMessage(request);
30
+ continue;
31
+ }
32
+ const plain = this.tryReadPlainJsonMessage();
33
+ if (plain !== null) {
34
+ const request = JSON.parse(plain);
35
+ console.error('[nullpay-mcp] parsed plain request', request.method, request.id);
36
+ await this.handleMessage(request);
37
+ continue;
38
+ }
39
+ return;
40
+ }
41
+ }
42
+ tryReadFramedMessage() {
43
+ let headerEnd = this.buffer.indexOf('\r\n\r\n');
44
+ let headerLength = 4;
45
+ if (headerEnd === -1) {
46
+ headerEnd = this.buffer.indexOf('\n\n');
47
+ headerLength = 2;
48
+ }
49
+ if (headerEnd === -1) {
50
+ return null;
51
+ }
52
+ const header = this.buffer.slice(0, headerEnd);
53
+ const contentLengthHeader = header
54
+ .split(/\r?\n/)
55
+ .find((line) => line.toLowerCase().startsWith('content-length:'));
56
+ if (!contentLengthHeader) {
57
+ return null;
58
+ }
59
+ const contentLength = Number(contentLengthHeader.split(':')[1].trim());
60
+ const messageStart = headerEnd + headerLength;
61
+ const messageEnd = messageStart + contentLength;
62
+ if (this.buffer.length < messageEnd) {
63
+ return null;
64
+ }
65
+ const message = this.buffer.slice(messageStart, messageEnd);
66
+ this.buffer = this.buffer.slice(messageEnd);
67
+ return message;
68
+ }
69
+ tryReadPlainJsonMessage() {
70
+ const trimmed = this.buffer.trim();
71
+ if (!trimmed.startsWith('{')) {
72
+ return null;
73
+ }
74
+ try {
75
+ JSON.parse(trimmed);
76
+ this.buffer = '';
77
+ return trimmed;
78
+ }
79
+ catch {
80
+ const newlineIndex = this.buffer.indexOf('\n');
81
+ if (newlineIndex === -1) {
82
+ return null;
83
+ }
84
+ const candidate = this.buffer.slice(0, newlineIndex).trim();
85
+ if (!candidate.startsWith('{')) {
86
+ this.buffer = this.buffer.slice(newlineIndex + 1);
87
+ return null;
88
+ }
89
+ JSON.parse(candidate);
90
+ this.buffer = this.buffer.slice(newlineIndex + 1);
91
+ return candidate;
92
+ }
93
+ }
94
+ async handleMessage(request) {
95
+ if (request.id === undefined || request.id === null) {
96
+ await this.onRequest(request);
97
+ return;
98
+ }
99
+ try {
100
+ const result = await this.onRequest(request);
101
+ console.error('[nullpay-mcp] sending response', request.method, request.id);
102
+ this.send({ jsonrpc: '2.0', id: request.id, result });
103
+ }
104
+ catch (error) {
105
+ console.error('[nullpay-mcp] request handler failed', request.method, request.id, error instanceof Error ? error.message : String(error));
106
+ this.send({
107
+ jsonrpc: '2.0',
108
+ id: request.id,
109
+ error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
110
+ });
111
+ }
112
+ }
113
+ send(payload) {
114
+ const body = JSON.stringify(payload);
115
+ const safeWrite = globalThis.__nullpayMcpStdoutWrite
116
+ || ((chunk) => process.stdout.write(chunk));
117
+ safeWrite(body + '\n');
118
+ }
119
+ }
120
+ exports.StdioJsonRpcServer = StdioJsonRpcServer;
@@ -0,0 +1 @@
1
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const backend_client_1 = require("./backend-client");
9
+ const protocol_1 = require("./protocol");
10
+ const session_store_1 = require("./session-store");
11
+ const service_1 = require("./service");
12
+ function parseDotEnv(content) {
13
+ const parsed = {};
14
+ for (const rawLine of content.split(/\r?\n/)) {
15
+ const line = rawLine.trim();
16
+ if (!line || line.startsWith('#')) {
17
+ continue;
18
+ }
19
+ const normalized = line.startsWith('export ') ? line.slice(7).trim() : line;
20
+ const separatorIndex = normalized.indexOf('=');
21
+ if (separatorIndex <= 0) {
22
+ continue;
23
+ }
24
+ const key = normalized.slice(0, separatorIndex).trim();
25
+ let value = normalized.slice(separatorIndex + 1).trim();
26
+ if (!key) {
27
+ continue;
28
+ }
29
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
30
+ value = value.slice(1, -1);
31
+ }
32
+ parsed[key] = value;
33
+ }
34
+ return parsed;
35
+ }
36
+ function loadEnvFiles() {
37
+ const packageRoot = path_1.default.resolve(__dirname, '..');
38
+ const repoRoot = path_1.default.resolve(packageRoot, '..', '..');
39
+ const candidates = [
40
+ path_1.default.resolve(process.cwd(), '.env'),
41
+ path_1.default.resolve(packageRoot, '.env'),
42
+ path_1.default.resolve(repoRoot, '.env'),
43
+ path_1.default.resolve(repoRoot, 'backend', '.env'),
44
+ ];
45
+ for (const filePath of candidates) {
46
+ if (!fs_1.default.existsSync(filePath)) {
47
+ continue;
48
+ }
49
+ const values = parseDotEnv(fs_1.default.readFileSync(filePath, 'utf8'));
50
+ for (const [key, value] of Object.entries(values)) {
51
+ if (!process.env[key]) {
52
+ process.env[key] = value;
53
+ }
54
+ }
55
+ }
56
+ }
57
+ function shieldStdoutForMcp() {
58
+ const protocolWrite = process.stdout.write.bind(process.stdout);
59
+ globalThis.__nullpayMcpStdoutWrite = (chunk) => protocolWrite(chunk);
60
+ process.stdout.write = ((chunk, encoding, callback) => {
61
+ const text = typeof chunk === 'string'
62
+ ? chunk
63
+ : Buffer.isBuffer(chunk)
64
+ ? chunk.toString('utf8')
65
+ : String(chunk);
66
+ process.stderr.write(text);
67
+ if (typeof encoding === 'function') {
68
+ encoding();
69
+ }
70
+ else if (typeof callback === 'function') {
71
+ callback();
72
+ }
73
+ return true;
74
+ });
75
+ }
76
+ loadEnvFiles();
77
+ shieldStdoutForMcp();
78
+ const backendBaseUrl = process.env.NULLPAY_BACKEND_URL || 'http://localhost:3000/api';
79
+ const publicBaseUrl = process.env.NULLPAY_PUBLIC_BASE_URL || 'https://nullpay.app';
80
+ const mcpSecret = process.env.NULLPAY_MCP_SHARED_SECRET;
81
+ const backend = new backend_client_1.NullPayBackendClient(backendBaseUrl, mcpSecret);
82
+ const sessions = new session_store_1.SessionStore();
83
+ const service = new service_1.NullPayMcpService(backend, sessions, publicBaseUrl);
84
+ const server = new protocol_1.StdioJsonRpcServer(async (request) => {
85
+ if (request.method === 'initialize') {
86
+ return {
87
+ protocolVersion: String(request.params?.protocolVersion || '2025-11-25'),
88
+ capabilities: {
89
+ tools: {}
90
+ },
91
+ serverInfo: {
92
+ name: 'nullpay-mcp',
93
+ version: '0.1.0'
94
+ }
95
+ };
96
+ }
97
+ if (request.method === 'notifications/initialized') {
98
+ return {};
99
+ }
100
+ if (request.method === 'tools/list') {
101
+ return { tools: service.listTools() };
102
+ }
103
+ if (request.method === 'tools/call') {
104
+ const name = String(request.params?.name || '');
105
+ const args = (request.params?.arguments || {});
106
+ return await service.callTool(name, args);
107
+ }
108
+ return {};
109
+ });
110
+ console.error('nullpay mcp starting');
111
+ server.start();
@@ -0,0 +1,179 @@
1
+ import { NullPayBackendClient } from './backend-client';
2
+ import { SessionStore } from './session-store';
3
+ import { ToolResult } from './types';
4
+ export declare class NullPayMcpService {
5
+ private readonly backend;
6
+ private readonly sessions;
7
+ private readonly publicBaseUrl;
8
+ constructor(backend: NullPayBackendClient, sessions: SessionStore, publicBaseUrl: string);
9
+ listTools(): ({
10
+ name: string;
11
+ description: string;
12
+ inputSchema: {
13
+ type: string;
14
+ properties: {
15
+ address: {
16
+ type: string;
17
+ description: string;
18
+ };
19
+ password: {
20
+ type: string;
21
+ description: string;
22
+ };
23
+ main_private_key: {
24
+ type: string;
25
+ description: string;
26
+ };
27
+ create_burner_wallet: {
28
+ type: string;
29
+ description: string;
30
+ };
31
+ wallet_preference: {
32
+ type: string;
33
+ enum: string[];
34
+ description: string;
35
+ };
36
+ amount?: undefined;
37
+ currency?: undefined;
38
+ memo?: undefined;
39
+ invoice_type?: undefined;
40
+ wallet?: undefined;
41
+ line_items?: undefined;
42
+ payment_link?: undefined;
43
+ invoice_hash?: undefined;
44
+ session_id?: undefined;
45
+ limit?: undefined;
46
+ };
47
+ required?: undefined;
48
+ };
49
+ } | {
50
+ name: string;
51
+ description: string;
52
+ inputSchema: {
53
+ type: string;
54
+ properties: {
55
+ amount: {
56
+ type: string;
57
+ description?: undefined;
58
+ };
59
+ currency: {
60
+ type: string;
61
+ enum: string[];
62
+ description?: undefined;
63
+ };
64
+ memo: {
65
+ type: string;
66
+ };
67
+ invoice_type: {
68
+ type: string;
69
+ enum: string[];
70
+ };
71
+ wallet: {
72
+ type: string;
73
+ enum: string[];
74
+ };
75
+ line_items: {
76
+ type: string;
77
+ };
78
+ address?: undefined;
79
+ password?: undefined;
80
+ main_private_key?: undefined;
81
+ create_burner_wallet?: undefined;
82
+ wallet_preference?: undefined;
83
+ payment_link?: undefined;
84
+ invoice_hash?: undefined;
85
+ session_id?: undefined;
86
+ limit?: undefined;
87
+ };
88
+ required: string[];
89
+ };
90
+ } | {
91
+ name: string;
92
+ description: string;
93
+ inputSchema: {
94
+ type: string;
95
+ properties: {
96
+ payment_link: {
97
+ type: string;
98
+ description: string;
99
+ };
100
+ invoice_hash: {
101
+ type: string;
102
+ description: string;
103
+ };
104
+ wallet: {
105
+ type: string;
106
+ enum: string[];
107
+ };
108
+ amount: {
109
+ type: string;
110
+ description: string;
111
+ };
112
+ currency: {
113
+ type: string;
114
+ enum: string[];
115
+ description: string;
116
+ };
117
+ session_id: {
118
+ type: string;
119
+ description: string;
120
+ };
121
+ address?: undefined;
122
+ password?: undefined;
123
+ main_private_key?: undefined;
124
+ create_burner_wallet?: undefined;
125
+ wallet_preference?: undefined;
126
+ memo?: undefined;
127
+ invoice_type?: undefined;
128
+ line_items?: undefined;
129
+ limit?: undefined;
130
+ };
131
+ required?: undefined;
132
+ };
133
+ } | {
134
+ name: string;
135
+ description: string;
136
+ inputSchema: {
137
+ type: string;
138
+ properties: {
139
+ invoice_hash: {
140
+ type: string;
141
+ description?: undefined;
142
+ };
143
+ wallet: {
144
+ type: string;
145
+ enum: string[];
146
+ };
147
+ limit: {
148
+ type: string;
149
+ };
150
+ address?: undefined;
151
+ password?: undefined;
152
+ main_private_key?: undefined;
153
+ create_burner_wallet?: undefined;
154
+ wallet_preference?: undefined;
155
+ amount?: undefined;
156
+ currency?: undefined;
157
+ memo?: undefined;
158
+ invoice_type?: undefined;
159
+ line_items?: undefined;
160
+ payment_link?: undefined;
161
+ session_id?: undefined;
162
+ };
163
+ required?: undefined;
164
+ };
165
+ })[];
166
+ callTool(name: string, args: Record<string, unknown>): Promise<ToolResult>;
167
+ private login;
168
+ private createInvoice;
169
+ private payInvoice;
170
+ private getTransactionInfo;
171
+ private resolveWallet;
172
+ private resolveWalletAddress;
173
+ private resolveWalletPrivateKey;
174
+ private resolveWalletPrivateKeyOptional;
175
+ private resolveInvoiceLookupPrivateKey;
176
+ private resolvePayInvoiceContext;
177
+ private enrichInvoiceIfPossible;
178
+ private getEnrichedInvoice;
179
+ }